From b693ed7c913e97331728079df520d3d7126f27f3 Mon Sep 17 00:00:00 2001 From: James Dawdy Date: Fri, 26 Jun 2026 10:56:52 -0500 Subject: [PATCH] fix: wrap unprotected evaluate() calls with robust_evaluate() to handle navigation context loss (#6677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: wrap unprotected evaluate() calls with robust_evaluate() to handle navigation context loss Fixes PR #5911 - Playwright UI test error: 'Execution context was destroyed' The test had several direct page.evaluate() and locator.evaluate() calls that weren't wrapped with robust_evaluate(), which retries when navigation destroys the execution context mid-operation. Changes: - Wrap picker_visible_text() evaluate in robust_evaluate() - Wrap _bubble_count() evaluate in robust_evaluate() - Wrap assistant text query in robust_evaluate() - Wrap theme_item click evaluation in robust_evaluate() - Wrap background color/theme query in robust_evaluate() This ensures all execution context losses from concurrent navigation are properly caught and retried with exponential backoff, preventing transient failures in the UI test suite. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: revert robust_evaluate on theme_item.evaluate per Codex review The theme_item.evaluate('el => el.click()') is side-effecting — retrying after a context loss could double-toggle the theme. It's already inside a 3-attempt try/except loop that handles click failures gracefully. The other 4 changes (all read-only queries) remain wrapped in robust_evaluate() since retrying them is safe. * fix: wrap remaining chat UI evaluate --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: imagineer99 --- tests/studio/playwright_chat_ui.py | 32 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/studio/playwright_chat_ui.py b/tests/studio/playwright_chat_ui.py index f9147054ee..a892d3414d 100644 --- a/tests/studio/playwright_chat_ui.py +++ b/tests/studio/playwright_chat_ui.py @@ -494,12 +494,15 @@ with sync_playwright() as p: # typeahead actually filters (else an ignored-input regression # would silently pass). def picker_visible_text(): - return page.evaluate("""() => { + return robust_evaluate( + page, + """() => { const el = document.querySelector( '[role="dialog"], [role="listbox"], [role="menu"]' ); return el ? (el.innerText || '').trim() : ''; - }""") + }""", + ) search.fill("qwen") page.wait_for_timeout(800) @@ -535,9 +538,12 @@ with sync_playwright() as p: def _bubble_count(): """Total [data-role='assistant'] elements (empty or not).""" - return page.evaluate("""() => { + return robust_evaluate( + page, + """() => { return document.querySelectorAll('[data-role="assistant"]').length; - }""") + }""", + ) def send_and_wait(prompt, idx): # 1. Wait until the previous turn fully stopped: Send attached @@ -626,8 +632,11 @@ with sync_playwright() as p: send_and_wait(p_, i) shoot("04-after-five-turns") - texts = page.evaluate("""() => Array.from(document.querySelectorAll('[data-role="assistant"]')) - .map(e => (e.innerText || '').trim())""") + texts = robust_evaluate( + page, + """() => Array.from(document.querySelectorAll('[data-role="assistant"]')) + .map(e => (e.innerText || '').trim())""", + ) if len(texts) < len(prompts): fail(f"expected >= {len(prompts)} assistant bubbles, got {len(texts)}") info(f"five turn lengths = {[len(t) for t in texts[:5]]}") @@ -840,7 +849,9 @@ with sync_playwright() as p: # Settle. The ".dark" class on is the ground truth # (theme-store toggles only that); don't gate on ".light". page.wait_for_timeout(700) - bg = page.evaluate("""() => { + bg = robust_evaluate( + page, + """() => { const root = document.documentElement; return { cls: root.className, @@ -848,7 +859,8 @@ with sync_playwright() as p: bg: getComputedStyle(document.body).backgroundColor, rbg: getComputedStyle(root).backgroundColor, }; - }""") + }""", + ) observed.append(bg) shoot(f"10-theme-cycle-{cycle + 1}") info(f" cycle {cycle + 1}: dark={bg['isDark']} body bg={bg['bg']!r}") @@ -1050,7 +1062,8 @@ with sync_playwright() as p: shoot("15d-recent-clicked") info(f"OK clicked recent entry: {t[:60]!r}") # The landed thread must include at least one of our prompts. - turns_text = page.evaluate( + turns_text = robust_evaluate( + page, """() => { const els = document.querySelectorAll( '[data-role="user"], [data-role="assistant"]' @@ -1058,7 +1071,6 @@ with sync_playwright() as p: return Array.from(els).map(e => (e.innerText || '') .toLowerCase()).join(' '); }""", - None, ) clicked_recent = True if any(k in turns_text for k in PROMPT_KEYWORDS):