"""Automated Playwright tests for SX docs demos. Covers hypermedia examples, reactive islands, and marshes. Run against the dev server: cd sx/tests pytest test_demos.py -v --headed # visible browser pytest test_demos.py -v # headless Requires: pip install pytest-playwright """ from __future__ import annotations import pytest from playwright.sync_api import Page, expect import os BASE = os.environ.get("SX_TEST_BASE", "http://localhost:8013") # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def browser_context_args(): return {"ignore_https_errors": True} def nav(page: Page, path: str): """Navigate to an SX URL and wait for rendered content. Captures JS errors during page load and fails immediately with the actual error message instead of waiting for a 30s timeout. """ js_errors: list[str] = [] page.on("pageerror", lambda err: js_errors.append(str(err))) page.goto(f"{BASE}/sx/{path}", wait_until="networkidle") # Poll briefly for JS errors — pageerror fires async during networkidle for _ in range(10): if js_errors: break page.wait_for_timeout(100) # Fail fast on JS errors — don't wait for content that will never appear if js_errors: pytest.fail(f"JS error on {path}: {js_errors[0]}") # Wait for SX to render — look for any heading or paragraph in main panel page.wait_for_selector("#main-panel h2, #main-panel p, #main-panel div", timeout=10000) # Check for rendering artifacts that indicate broken serialization main_text = page.locator("#main-panel").text_content() or "" if "[object Object]" in main_text: pytest.fail(f"Rendering artifact on {path}: [object Object] in page content") # --------------------------------------------------------------------------- # Hypermedia Examples # --------------------------------------------------------------------------- class TestClickToLoad: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.click-to-load)))") expect(page.locator("#main-panel")).to_contain_text("Click to Load") def test_click_loads_content(self, page: Page): nav(page, "(geography.(hypermedia.(example.click-to-load)))") page.click("button:has-text('Load content')") expect(page.locator("#click-result")).to_contain_text( "Content loaded", timeout=5000 ) class TestFormSubmission: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.form-submission)))") expect(page.locator("#main-panel")).to_contain_text("Form Submission") def test_has_code_examples(self, page: Page): """Page must show component source and handler source code.""" nav(page, "(geography.(hypermedia.(example.form-submission)))") code_blocks = page.locator("pre code") expect(code_blocks.first).not_to_be_empty(timeout=5000) # Should have at least component code and handler code count = code_blocks.count() assert count >= 2, f"Expected at least 2 code blocks, got {count}" # Handler code should contain defhandler all_code = " ".join(code_blocks.nth(i).text_content() or "" for i in range(count)) assert "defhandler" in all_code or "sx-post" in all_code, \ f"Code blocks should contain handler or component source" def test_submit_form(self, page: Page): nav(page, "(geography.(hypermedia.(example.form-submission)))") page.fill("input[name='name']", "TestUser") page.click("button[type='submit']") expect(page.locator("#form-result")).to_contain_text("TestUser", timeout=5000) class TestPolling: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.polling)))") expect(page.locator("#main-panel")).to_contain_text("Polling") def test_poll_updates(self, page: Page): nav(page, "(geography.(hypermedia.(example.polling)))") expect(page.locator("#poll-target")).to_contain_text("Server time", timeout=10000) class TestDeleteRow: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.delete-row)))") expect(page.locator("#main-panel")).to_contain_text("Delete Row") def test_delete_removes_row(self, page: Page): nav(page, "(geography.(hypermedia.(example.delete-row)))") rows = page.locator("#delete-rows tr") initial_count = rows.count() assert initial_count >= 2, f"Expected at least 2 rows, got {initial_count}" # Accept the confirm dialog page.on("dialog", lambda d: d.accept()) rows.first.locator("button:has-text('delete')").click() expect(rows).to_have_count(initial_count - 1, timeout=5000) class TestInlineEdit: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.inline-edit)))") expect(page.locator("#main-panel")).to_contain_text("Inline Edit") def test_edit_shows_form(self, page: Page): nav(page, "(geography.(hypermedia.(example.inline-edit)))") page.click("#edit-target button:has-text('edit')") expect(page.locator("input[name='value']")).to_be_visible(timeout=5000) class TestOobSwaps: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.oob-swaps)))") expect(page.locator("#main-panel")).to_contain_text("OOB") def test_oob_updates_both_boxes(self, page: Page): nav(page, "(geography.(hypermedia.(example.oob-swaps)))") page.click("button:has-text('Update both boxes')") expect(page.locator("#oob-box-a")).to_contain_text("Box A updated", timeout=5000) expect(page.locator("#oob-box-b").last).to_contain_text("Box B updated", timeout=5000) class TestLazyLoading: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.lazy-loading)))") expect(page.locator("#main-panel")).to_contain_text("Lazy Loading") def test_content_loads_automatically(self, page: Page): nav(page, "(geography.(hypermedia.(example.lazy-loading)))") expect(page.locator("#lazy-target")).to_contain_text("Content loaded", timeout=10000) class TestInfiniteScroll: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.infinite-scroll)))") expect(page.locator("#main-panel")).to_contain_text("Infinite Scroll") class TestProgressBar: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.progress-bar)))") expect(page.locator("#main-panel")).to_contain_text("Progress Bar") def test_progress_starts(self, page: Page): nav(page, "(geography.(hypermedia.(example.progress-bar)))") page.click("button:has-text('Start job')") expect(page.locator("#progress-target")).to_contain_text("complete", timeout=10000) class TestActiveSearch: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.active-search)))") expect(page.locator("#main-panel")).to_contain_text("Active Search") def test_search_returns_results(self, page: Page): nav(page, "(geography.(hypermedia.(example.active-search)))") page.locator("input[name='q']").type("Py") expect(page.locator("#search-results")).to_contain_text("Python", timeout=5000) class TestInlineValidation: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.inline-validation)))") expect(page.locator("#main-panel")).to_contain_text("Inline Validation") def test_invalid_email(self, page: Page): nav(page, "(geography.(hypermedia.(example.inline-validation)))") page.fill("input[name='email']", "notanemail") page.locator("input[name='email']").blur() expect(page.locator("#email-feedback")).to_contain_text("Invalid", timeout=5000) class TestValueSelect: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.value-select)))") expect(page.locator("#main-panel")).to_contain_text("Value Select") def test_select_changes_options(self, page: Page): nav(page, "(geography.(hypermedia.(example.value-select)))") page.select_option("select[name='category']", "Languages") expect(page.locator("#value-items")).to_contain_text("Python", timeout=5000) class TestResetOnSubmit: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.reset-on-submit)))") expect(page.locator("#main-panel")).to_contain_text("Reset") def test_has_form(self, page: Page): nav(page, "(geography.(hypermedia.(example.reset-on-submit)))") expect(page.locator("form")).to_be_visible(timeout=5000) class TestEditRow: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.edit-row)))") expect(page.locator("#main-panel")).to_contain_text("Edit Row") class TestBulkUpdate: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.bulk-update)))") expect(page.locator("#main-panel")).to_contain_text("Bulk Update") def test_has_checkboxes(self, page: Page): nav(page, "(geography.(hypermedia.(example.bulk-update)))") expect(page.locator("input[type='checkbox']").first).to_be_visible(timeout=5000) class TestSwapPositions: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.swap-positions)))") expect(page.locator("#main-panel")).to_contain_text("Swap") class TestSelectFilter: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.select-filter)))") expect(page.locator("#main-panel")).to_contain_text("Select Filter") class TestTabs: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.tabs)))") expect(page.locator("#main-panel")).to_contain_text("Tabs") class TestAnimations: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.animations)))") expect(page.locator("#main-panel")).to_contain_text("Animation") class TestDialogs: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.dialogs)))") expect(page.locator("#main-panel")).to_contain_text("Dialog") class TestKeyboardShortcuts: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.keyboard-shortcuts)))") expect(page.locator("#main-panel")).to_contain_text("Keyboard") class TestPutPatch: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.put-patch)))") expect(page.locator("#main-panel")).to_contain_text("PUT") class TestJsonEncoding: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.json-encoding)))") expect(page.locator("#main-panel")).to_contain_text("JSON") class TestValsAndHeaders: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.vals-and-headers)))") expect(page.locator("#main-panel")).to_contain_text("Vals") class TestLoadingStates: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.loading-states)))") expect(page.locator("#main-panel")).to_contain_text("Loading") class TestSyncReplace: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.sync-replace)))") expect(page.locator("#main-panel")).to_contain_text("Request Abort") class TestRetry: def test_page_loads(self, page: Page): nav(page, "(geography.(hypermedia.(example.retry)))") expect(page.locator("#main-panel")).to_contain_text("Retry") # --------------------------------------------------------------------------- # Reactive Islands # --------------------------------------------------------------------------- class TestReactiveIslandsOverview: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive))") expect(page.locator("#main-panel")).to_contain_text("Reactive Islands") def test_architecture_table(self, page: Page): nav(page, "(geography.(reactive))") expect(page.locator("table").first).to_be_visible() class TestReactiveExamplesOverview: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples)))") expect(page.locator("#main-panel")).to_contain_text("Examples") class TestReactiveCounter: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.counter)))") expect(page.locator("#main-panel")).to_contain_text("signal holds a value") def test_counter_increments(self, page: Page): nav(page, "(geography.(reactive.(examples.counter)))") # Find the island's + button island = page.locator("[data-sx-island*='demo-counter']") expect(island).to_be_visible(timeout=5000) # Wait for hydration page.wait_for_timeout(1000) initial = island.locator("span.text-2xl").text_content() island.locator("button", has_text="+").click() page.wait_for_timeout(500) updated = island.locator("span.text-2xl").text_content() assert initial != updated, f"Counter should change: {initial} -> {updated}" class TestReactiveTemperature: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.temperature)))") expect(page.locator("#main-panel")).to_contain_text("Temperature") def test_temperature_updates(self, page: Page): nav(page, "(geography.(reactive.(examples.temperature)))") island = page.locator("[data-sx-island*='demo-temperature']") expect(island).to_be_visible(timeout=5000) page.wait_for_timeout(1000) island.locator("button", has_text="+5").click() page.wait_for_timeout(500) # Should show celsius value > 20 (default) expect(island).to_contain_text("25") class TestReactiveStopwatch: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.stopwatch)))") expect(page.locator("#main-panel")).to_contain_text("Stopwatch") class TestReactiveImperative: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.imperative)))") expect(page.locator("#main-panel")).to_contain_text("Imperative") class TestReactiveList: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.reactive-list)))") expect(page.locator("#main-panel")).to_contain_text("Reactive List") def test_add_item(self, page: Page): nav(page, "(geography.(reactive.(examples.reactive-list)))") island = page.locator("[data-sx-island*='demo-reactive-list']") expect(island).to_be_visible(timeout=5000) page.wait_for_timeout(1000) island.locator("button", has_text="Add Item").click() page.wait_for_timeout(500) expect(island).to_contain_text("Item 1") class TestReactiveInputBinding: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.input-binding)))") expect(page.locator("#main-panel")).to_contain_text("Input Binding") def test_input_binding(self, page: Page): nav(page, "(geography.(reactive.(examples.input-binding)))") island = page.locator("[data-sx-island*='demo-input-binding']") expect(island).to_be_visible(timeout=5000) page.wait_for_timeout(1000) island.locator("input[type='text']").fill("World") page.wait_for_timeout(500) expect(island).to_contain_text("Hello, World!") class TestReactivePortal: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.portal)))") expect(page.locator("#main-panel")).to_contain_text("Portal") class TestReactiveErrorBoundary: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.error-boundary)))") expect(page.locator("#main-panel")).to_contain_text("Error") class TestReactiveRefs: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.refs)))") expect(page.locator("#main-panel")).to_contain_text("Refs") class TestReactiveDynamicClass: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.dynamic-class)))") expect(page.locator("#main-panel")).to_contain_text("Dynamic") class TestReactiveResource: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.resource)))") expect(page.locator("#main-panel")).to_contain_text("Resource") class TestReactiveTransition: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.transition)))") expect(page.locator("#main-panel")).to_contain_text("Transition") class TestReactiveStores: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.stores)))") expect(page.locator("#main-panel")).to_contain_text("Store") class TestReactiveEventBridge: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.event-bridge-demo)))") expect(page.locator("#main-panel")).to_contain_text("Event Bridge") class TestReactiveDefisland: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.defisland)))") expect(page.locator("#main-panel")).to_contain_text("defisland") class TestReactiveTests: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.tests)))") expect(page.locator("#main-panel")).to_contain_text("test") class TestReactiveCoverage: def test_page_loads(self, page: Page): nav(page, "(geography.(reactive.(examples.coverage)))") expect(page.locator("#main-panel")).to_contain_text("React") # --------------------------------------------------------------------------- # Marshes # --------------------------------------------------------------------------- class TestMarshesOverview: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes))") expect(page.locator("#main-panel")).to_contain_text("Marshes") class TestMarshesHypermediaFeeds: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes.hypermedia-feeds))") expect(page.locator("#main-panel")).to_contain_text("Hypermedia Feeds") class TestMarshesServerSignals: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes.server-signals))") expect(page.locator("#main-panel")).to_contain_text("Server Writes") class TestMarshesOnSettle: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes.on-settle))") expect(page.locator("#main-panel")).to_contain_text("sx-on-settle") class TestMarshesSignalTriggers: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes.signal-triggers))") expect(page.locator("#main-panel")).to_contain_text("Signal-Bound") class TestMarshesViewTransform: def test_page_loads(self, page: Page): nav(page, "(geography.(marshes.view-transform))") expect(page.locator("#main-panel")).to_contain_text("Reactive View") # --------------------------------------------------------------------------- # Spec Explorer # --------------------------------------------------------------------------- class TestSpecExplorer: def test_evaluator_server_renders(self, page: Page): """Server returns spec explorer content with evaluator data.""" resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.evaluator)))") assert resp.ok, f"Server returned {resp.status}" body = resp.text() assert "Evaluator" in body, "Should contain evaluator title" assert "eval" in body.lower(), "Should contain evaluator content" def test_parser_server_renders(self, page: Page): """Server returns spec explorer content with parser data.""" resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.parser)))") assert resp.ok, f"Server returned {resp.status}" body = resp.text() assert "Parser" in body, "Should contain parser title" def test_has_spec_source(self, page: Page): """Spec explorer includes actual spec source code.""" resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.evaluator)))") body = resp.text() assert "define" in body, "Should contain define forms from spec" assert "eval-expr" in body, "Should contain eval-expr from evaluator spec" @pytest.mark.xfail(reason="Client re-evaluates defpage content; find-spec unavailable on client") def test_evaluator_renders_in_browser(self, page: Page): """Spec explorer should render correctly in the browser, not show 'not found'.""" nav(page, "(language.(spec.(explore.evaluator)))") page.wait_for_timeout(3000) content = page.locator("#main-panel").text_content() or "" assert "not found" not in content.lower(), \ f"Page shows 'not found' instead of spec content: {content[:200]}" expect(page.locator("#main-panel")).to_contain_text("Evaluator", timeout=5000) # --------------------------------------------------------------------------- # Key doc pages (smoke tests) # --------------------------------------------------------------------------- def _check_no_fatal_errors(errors): """Assert no fatal JS errors were collected.""" fatal = [e for e in errors if any(s in e for s in ("Not callable", "Undefined symbol", "SES_UNCAUGHT", "UNCAUGHT"))] assert not fatal, f"JS errors: {fatal}" def _setup_error_capture(page): """Attach error listeners, return the errors list.""" errors = [] page.on("pageerror", lambda err: errors.append(f"UNCAUGHT: {err.message}")) page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None) return errors class TestHomePage: def test_home_has_header(self, page: Page): """Home page MUST have the () header with logo.""" errors = _setup_error_capture(page) nav(page, "") # Header logo — the () text expect(page.locator("#logo-opacity")).to_be_visible(timeout=15000) _check_no_fatal_errors(errors) def test_home_has_nav_children(self, page: Page): """Home page MUST have nav children (Geography, Language, etc.).""" nav(page, "") expect(page.locator("a[sx-push-url]:has-text('Geography')")).to_be_visible(timeout=15000) def test_home_has_main_panel(self, page: Page): """Home page MUST render #main-panel with content inside it.""" nav(page, "") expect(page.locator("#main-panel")).to_be_visible(timeout=15000) # Must have actual content (divs/headings), not be empty page.wait_for_selector("#main-panel div, #main-panel h2, #main-panel p", timeout=15000) def test_no_console_errors(self, page: Page): """Home page should have no JS errors (console or uncaught).""" errors = _setup_error_capture(page) page.goto(f"{BASE}/sx/", wait_until="networkidle") page.wait_for_timeout(3000) _check_no_fatal_errors(errors) def test_navigate_from_home_to_geography(self, page: Page): """Click Geography nav link from home — content must render.""" errors = _setup_error_capture(page) nav(page, "") geo_link = page.locator("a[sx-push-url]:has-text('Geography')").first expect(geo_link).to_be_visible(timeout=10000) geo_link.click() page.wait_for_timeout(5000) expect(page.locator("#main-panel")).to_contain_text("Geography", timeout=10000) _check_no_fatal_errors(errors) def test_navigate_geography_to_reactive(self, page: Page): """Click Reactive Islands from Geography — content must render.""" errors = _setup_error_capture(page) nav(page, "(geography)") ri_link = page.locator("a[sx-push-url]:has-text('Reactive Islands')").first expect(ri_link).to_be_visible(timeout=10000) ri_link.click() page.wait_for_timeout(5000) expect(page.locator("#main-panel")).to_contain_text("Reactive Islands", timeout=10000) _check_no_fatal_errors(errors) class TestDirectPageLoad: """Direct page loads (not HTMX) — each uses a FRESH browser context to avoid stale component hash in localStorage from prior tests.""" @pytest.mark.parametrize("path,expected_text", [ ("(geography.(reactive))", "Reactive Islands"), ("(etc)", "Etc"), ("(geography)", "Geography"), ]) def test_page_renders_with_header(self, browser, path: str, expected_text: str): """Page must have header, #main-panel, and expected content.""" ctx = browser.new_context(ignore_https_errors=True) page = ctx.new_page() try: errors = _setup_error_capture(page) page.goto(f"{BASE}/sx/{path}", wait_until="networkidle") page.wait_for_selector("#main-panel", timeout=30000) expect(page.locator("#main-panel")).to_contain_text(expected_text, timeout=10000) expect(page.locator("#sx-nav")).to_be_visible(timeout=5000) _check_no_fatal_errors(errors) finally: ctx.close() class TestDocPages: @pytest.mark.parametrize("path,expected", [ ("(geography.(reactive))", "Reactive Islands"), ("(geography.(hypermedia.(reference.attributes)))", "Attributes"), ("(geography.(scopes))", "Scopes"), ("(geography.(provide))", "Provide"), ("(geography.(spreads))", "Spreads"), ("(language.(doc.introduction))", "Introduction"), ("(language.(spec.core))", "Core"), ("(applications.(cssx))", "CSSX"), ("(etc.(essay.why-sexps))", "Why S-Expressions"), ]) def test_page_loads(self, page: Page, path: str, expected: str): nav(page, path) expect(page.locator("#main-panel")).to_contain_text(expected, timeout=10000) # --------------------------------------------------------------------------- # Navigation tests # --------------------------------------------------------------------------- class TestClientNavigation: def test_navigate_between_reactive_examples(self, page: Page): """Navigate from counter to temperature via server fetch.""" nav(page, "(geography.(reactive.(examples.counter)))") expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000) # Click temperature link in sibling nav — has sx-get for server fetch temp_link = page.locator("a[sx-push-url]:has-text('Temperature')").first expect(temp_link).to_be_visible(timeout=5000) temp_link.click() page.wait_for_timeout(5000) expect(page.locator("#main-panel")).to_contain_text("Temperature", timeout=10000) def test_reactive_island_works_after_navigation(self, page: Page): """Island should be functional after client-side navigation.""" nav(page, "(geography.(reactive.(examples.temperature)))") page.wait_for_timeout(3000) # Wait for hydration # Click +5 and verify island = page.locator("[data-sx-island*='demo-temperature']") expect(island).to_be_visible(timeout=5000) island.locator("button", has_text="+5").click() page.wait_for_timeout(500) expect(island).to_contain_text("25") def test_full_refresh_preserves_content(self, page: Page): """Navigate to a page, then hard-refresh — content must re-render.""" errors = _setup_error_capture(page) nav(page, "(geography.(hypermedia.(example.click-to-load)))") expect(page.locator("#main-panel")).to_contain_text("Click to Load") # Hard refresh page.reload(wait_until="networkidle") page.wait_for_timeout(2000) expect(page.locator("#main-panel")).to_contain_text("Click to Load", timeout=10000) _check_no_fatal_errors(errors) def test_back_button_after_navigation(self, page: Page): """Navigate A → B, then back — A must re-render without errors.""" errors = _setup_error_capture(page) nav(page, "(geography.(reactive.(examples.counter)))") expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000) # Navigate to temperature temp_link = page.locator("a[sx-push-url]:has-text('Temperature')").first expect(temp_link).to_be_visible(timeout=5000) temp_link.click() page.wait_for_timeout(3000) expect(page.locator("#main-panel")).to_contain_text("Temperature", timeout=10000) # Go back page.go_back() page.wait_for_timeout(3000) expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000) _check_no_fatal_errors(errors) def test_direct_load_no_js_errors(self, browser): """Fresh browser context — direct page load must have zero JS errors.""" ctx = browser.new_context(ignore_https_errors=True) page = ctx.new_page() try: errors = _setup_error_capture(page) page.goto(f"{BASE}/sx/(geography.(hypermedia.(example.delete-row)))", wait_until="networkidle") page.wait_for_timeout(3000) page.wait_for_selector("#main-panel", timeout=15000) _check_no_fatal_errors(errors) finally: ctx.close()