diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 7007338f6..c62b90d83 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -280,6 +280,13 @@ If a race appears only on a loaded machine, make the ordering explicit with the route before the gesture whose request it must catch. For initial navigation, attach it through `primed` so no request is already in flight. +That rule is about the page. A fact the driver loses on its way out of the browser +is not a page state any route can arrange, and it has no second channel to be read +through: `opened_tab` makes the press again because Chromium made the tab every time +and Playwright reported none of the lost ones. Reach for a repeat only with that +evidence in hand — the browser's own record showing the subject did its part — and +say so where the repeat is written. + A handler that appends a route to `held` has established only that the browser made the request. Before indexing `held`, wait for the corresponding `Traffic` edge, a request event, or another fact named by the handler. Some resources are requested diff --git a/tests/render_harness.py b/tests/render_harness.py index 4faf0b322..a78f308f6 100644 --- a/tests/render_harness.py +++ b/tests/render_harness.py @@ -908,6 +908,56 @@ def open_page( return page, errors +def opened_tab(page, press, tries=3, each=10_000): + """The tab a press opens, pressed again when the harness loses the one Chromium made. + + Chromium makes the tab every time. After a press whose tab never arrives, + `Target.getTargets` holds a second page target in this browser context — attached, + loaded, titled, sitting at the href the press named — while `context.pages` still + holds one, and it stays that way for the life of the page: the targets pile up press + after press and Playwright reports none of them. So `expect_page` spends its whole + timeout waiting on a tab that already exists, and the test reads as though the press + had opened nothing. + + A driver that loses the handle is not a page state a route can arrange, and there is + no second channel to reach an unreported tab through, so the press is made again + rather than waited on longer. This is instrument repair, not tolerance for a flaky + subject: the press itself is deterministic — the loss reaches every chord, though not + at one rate: 3, 10 and 1 of 60 presses lost for ⌃-click, ⌃⇧-click and ⇧-click on a + loaded machine — so a runtime that stopped leaving a real href for the platform to act + on opens no tab for any of the tries, and the last one says which wait went unanswered. + + A press whose tab is lost still leaves that tab loaded and polling its server, and no + caller can close what it was never handed. A test that closes the tab it receives is + closing only the try that was reported; the rest stand until context teardown. That is + the standing cost of the repeat, not a leak to chase. + + A press that refuses outright — an anchor hidden, covered, or disabled, the shape a + runtime regression takes — raises its own timeout from inside the wait, and Playwright's + `EventContextManager.__exit__` cancels the wait and lets it through. Repeating that + press would be exactly the tolerance this helper is not, so it is caught where it is + raised and named as the subject's refusal. + """ + for attempt in range(tries): + try: + with page.context.expect_page(timeout=each) as opened: + try: + press() + except PlaywrightTimeout as refused: + raise AssertionError( + "the press timed out before any tab could open: the subject " + "refused the gesture, which is not the loss this repeats for" + ) from refused + return opened.value + except PlaywrightTimeout as lost: + if attempt == tries - 1: + raise AssertionError( + f"no tab after {tries} presses waiting {each}ms each: either the " + "press stopped leaving a real href, or Chromium holds a target " + "Playwright never reported" + ) from lost + + def primed(browser, prepare): """A browser whose pages reach the product with the suite's hands already on them. diff --git a/tests/render_support.py b/tests/render_support.py index 4bf84f1d9..ce08a9094 100644 --- a/tests/render_support.py +++ b/tests/render_support.py @@ -276,6 +276,7 @@ leaf_page, navigate, open_page, + opened_tab, page_registry, panel_settled, post_event, @@ -537,6 +538,7 @@ "navigate", "one_reader", "open_page", + "opened_tab", "other_leaf", "page_at_rest", "page_registry", diff --git a/tests/test_render_controls.py b/tests/test_render_controls.py index 6e7048621..6dcde32ea 100644 --- a/tests/test_render_controls.py +++ b/tests/test_render_controls.py @@ -78,6 +78,7 @@ live_watcher, mark_edges, open_page, + opened_tab, page_at_rest, page_registry, panel_settled, @@ -1768,12 +1769,11 @@ def test_the_banner_opens_a_panel_of_the_machines_leaves( f"The other leaf\n{tmp_path / 'other-work'}\nWorking — running the suite", ) destination = link.get_attribute("href") - with page.context.expect_page() as opened: - link.click() + tab = opened_tab(page, link.click) # The new tab keeps the other page's live root, authorized by the key its link # carried, rather than being redirected onto one immutable version. assert destination is not None and destination.startswith(f"{other_url}/?t=") - expect(opened.value).to_have_url(destination) + expect(tab).to_have_url(destination) # The press left this tab alone, tray still standing. expect(others_panel).to_be_visible() page.keyboard.press("Escape") @@ -1943,10 +1943,9 @@ def test_the_leaves_tray_takes_the_keyboard(browser, serve, live_leaf): # Enter is the browser's own on a link, which is why the row is one. page.keyboard.press("ArrowDown") destination = rows.nth(1).get_attribute("href") - with page.context.expect_page() as opened: - page.keyboard.press("Enter") + tab = opened_tab(page, lambda: page.keyboard.press("Enter")) assert destination is not None and destination.startswith(f"{other_url}/?t=") - expect(opened.value).to_have_url(destination) + expect(tab).to_have_url(destination) page.keyboard.press("Escape") expect(page.locator(".lf-others-panel")).not_to_be_visible() # Closing while focus is inside would drop the reader on the body; it lands on diff --git a/tests/test_render_projection.py b/tests/test_render_projection.py index ac858d0de..e82fd2f32 100644 --- a/tests/test_render_projection.py +++ b/tests/test_render_projection.py @@ -51,6 +51,7 @@ leaf_page, live_url, open_page, + opened_tab, page_registry, painted, panel_settled, @@ -2354,9 +2355,7 @@ def test_a_message_reference_travels_or_says_it_cant(browser, serve): # holds — ⌘ where it was written, ⌃ where CI runs it — so the press names the # gesture and lets Playwright spell it. Named outright, the Linux press opened # nothing at all and the wait for the tab ran its full 30s before saying so. - with page.context.expect_page() as opened: - live.click(modifiers=["ControlOrMeta"]) - tab = opened.value + tab = opened_tab(page, lambda: live.click(modifiers=["ControlOrMeta"])) tab.wait_for_function(BOTH_STAMPS) tab.wait_for_function( """() => { const r = document.getElementById('p-bath').getBoundingClientRect();