Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions tests/integration/tests_playwright/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Conftest for Playwright integration tests.

Records browser-side activity (console messages, page errors, websocket
frames) for each test's page and attaches it to the test report on failure,
so CI logs contain enough context to diagnose flaky frontend behavior
post-mortem.
"""

import time

import pytest

# Cap recorded entries per test so a chatty page can't bloat memory or logs.
_MAX_DIAGNOSTIC_ENTRIES = 500
_MAX_ENTRY_LENGTH = 300


@pytest.fixture(autouse=True)
def _page_diagnostics(request):
"""Capture console/pageerror/websocket activity from the ``page`` fixture.

Args:
request: The pytest fixture request object.

Yields:
Control to the test function.
"""
if "page" not in request.fixturenames:
yield
return
page = request.getfixturevalue("page")
log: list[str] = []
t0 = time.monotonic()

def stamp(message: str) -> None:
if len(log) < _MAX_DIAGNOSTIC_ENTRIES:
log.append(f"+{time.monotonic() - t0:.3f}s {message[:_MAX_ENTRY_LENGTH]}")

page.on("console", lambda msg: stamp(f"console.{msg.type}: {msg.text}"))
page.on("pageerror", lambda exc: stamp(f"pageerror: {exc}"))

def _on_websocket(ws) -> None:
stamp(f"ws open: {ws.url}")
ws.on("framesent", lambda frame: stamp(f"ws sent: {frame}"))
ws.on("framereceived", lambda frame: stamp(f"ws recv: {frame}"))
Comment on lines +44 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The frame argument passed to framesent/framereceived callbacks is a Playwright WebSocketFrame object. It does not override __repr__ or __str__, so interpolating it directly with an f-string logs an opaque object address (e.g. <WebSocketFrame object at 0x7f…>) rather than the actual payload. Use frame.payload to capture the frame contents that make these diagnostics useful.

Suggested change
ws.on("framesent", lambda frame: stamp(f"ws sent: {frame}"))
ws.on("framereceived", lambda frame: stamp(f"ws recv: {frame}"))
ws.on("framesent", lambda frame: stamp(f"ws sent: {frame.payload}"))
ws.on("framereceived", lambda frame: stamp(f"ws recv: {frame.payload}}"))

ws.on("close", lambda _ws: stamp("ws close"))

page.on("websocket", _on_websocket)
request.node._page_diagnostics = log
yield


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Attach recorded page diagnostics to failed (or rerun) call reports.

Args:
item: The test item being reported on.
call: The call info for the current test phase.

Yields:
Control to other report hooks.
"""
outcome = yield
report = outcome.get_result()
if report.when != "call" or not (report.failed or report.outcome == "rerun"):
return
log = getattr(item, "_page_diagnostics", None)
if log:
report.sections.append((f"page diagnostics ({report.outcome})", "\n".join(log)))
37 changes: 27 additions & 10 deletions tests/integration/tests_playwright/test_memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ def keyed_row(label: rx.Var[str]) -> rx.Component:

def index() -> rx.Component:
return rx.vstack(
rx.input(
value=MemoState.router.session.client_token,
read_only=True,
id="token",
),
rx.text(MemoState.last_value, id="memo-last-value"),
my_memoed_component(
some_value="memod_some_value", event=MemoState.set_last_value
Expand Down Expand Up @@ -125,6 +130,23 @@ def memo_app(
yield harness


def load_page(memo_app: AppHarness, page: Page) -> None:
"""Navigate to the app and wait for hydration before interacting.

The ``#token`` input is bound to router state, so it only gets a value
once the websocket is connected and the hydrate round trip has finished.
Interacting before that point races the event-loop startup (the typed
input event can be lost), which made these tests flaky in CI.

Args:
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
expect(page.locator("#token")).not_to_have_value("")


def test_memo_event_handler_partial_application(
memo_app: AppHarness, page: Page
) -> None:
Expand All @@ -134,8 +156,7 @@ def test_memo_event_handler_partial_application(
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
load_page(memo_app, page)

expect(page.locator("#memo-last-value")).to_have_text("")
page.click("#memo-button")
Expand All @@ -149,8 +170,7 @@ def test_memo_event_handler_raw_pass_through(memo_app: AppHarness, page: Page) -
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
load_page(memo_app, page)

page.locator("#memo-input").fill("typed_value")
expect(page.locator("#memo-last-value")).to_have_text("typed_value")
Expand All @@ -163,8 +183,7 @@ def test_memo_recursive_tree_render(memo_app: AppHarness, page: Page) -> None:
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
load_page(memo_app, page)

tree_root = page.locator("#tree-root")
node_names = tree_root.locator(".tree-node-name")
Expand All @@ -179,8 +198,7 @@ def test_memo_recursive_tree_reacts_to_state(memo_app: AppHarness, page: Page) -
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
load_page(memo_app, page)

node_names = page.locator("#tree-root .tree-node-name")
expect(node_names).to_have_count(4)
Expand All @@ -206,8 +224,7 @@ def test_memo_key_preserves_identity_across_reorder(
memo_app: Running app harness.
page: Playwright page.
"""
assert memo_app.frontend_url is not None
page.goto(memo_app.frontend_url)
load_page(memo_app, page)

rows = page.locator("#keyed-rows input")
expect(rows).to_have_count(3)
Expand Down
Loading