From 9bf0c6ff09dad6f75c671e1bc95ccc8f2ea6d6d0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 7 May 2026 13:13:15 +0000 Subject: [PATCH] ci(mac): wait_for_load_state before change-password form + drop pre-fill shoot Run 25497245250 / job 74820324136 (commit f3e541d) failed with: Page.fill: Timeout 60000ms exceeded. Call log: - waiting for locator("#new-password") This was AFTER `page.locator("#new-password").wait_for(state="visible")` returned successfully. So the element WAS visible at that moment, then disappeared from the DOM 60s before page.fill could grab it. Root cause: on macos-14 free runners under --single-process Chromium, the change-password page's bootstrap-state poll (/api/auth/status) and React router both finish AFTER wait_for() returns. If they decide the user is "already authenticated" or "no longer must change password", the route rerenders and the #new-password input is unmounted. Page.fill then waits the full 60s for an element that's gone. Two changes (both playwright_chat_ui.py and playwright_extra_ui.py): 1. Add `page.wait_for_load_state("networkidle", timeout=30_000)` AFTER page.goto, BEFORE wait_for(). This lets the bootstrap dispatch settle so the route is committed before we touch the form. Wrapped in try/except so a slow `networkidle` (e.g. SSE keepalives) doesn't block forever -- best-effort. 2. Drop the `shoot("01-change-password-initial")` call between wait_for() and fill(). The screenshot's font-load wait is another window for the React form to detach. The `02-change-password-filled` shoot AFTER the fill is sufficient for diagnostics. Use locator API + explicit per-call timeouts. --- tests/studio/playwright_chat_ui.py | 29 ++++++++++++++++++++++++----- tests/studio/playwright_extra_ui.py | 14 +++++++++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/studio/playwright_chat_ui.py b/tests/studio/playwright_chat_ui.py index 7f3768b977..8ec5157d0e 100644 --- a/tests/studio/playwright_chat_ui.py +++ b/tests/studio/playwright_chat_ui.py @@ -257,11 +257,30 @@ with sync_playwright() as p: # ───────────────────────────────────────────────────── step("change-password through UI (Setup your account)") page.goto(f"{BASE}/change-password") - page.locator("#new-password").wait_for(state = "visible", timeout = 60_000) - shoot("01-change-password-initial") - page.fill("#new-password", NEW) - page.fill("#confirm-password", NEW) - shoot("02-change-password-filled") + # Wait for the network to settle before touching the form. Without + # this, on macos-14 free runners under --single-process Chromium, + # the page sometimes redirects mid-test (the bootstrap state poll + # finishes after wait_for() returns, the React router decides + # we're "already authenticated" or "no longer must-change", and + # rerenders without #new-password). Letting the network idle first + # gives the bootstrap dispatch a chance to settle BEFORE we + # commit to the form path. Run 25497245250 / job 74820324136 + # showed this exact sequence: wait_for() returned then + # page.fill('#new-password') timed out 60s later because the + # form had been replaced. + try: + page.wait_for_load_state("networkidle", timeout = 30_000) + except Exception: + pass # best-effort -- proceed even if network never idles + pw_field = page.locator("#new-password") + pw_field.wait_for(state = "visible", timeout = 60_000) + # NOTE: do NOT call shoot() between wait_for and fill -- the + # screenshot's font-load wait gives the React form a chance to + # detach if any background state-poll fires. Take screenshots + # AFTER the form is committed instead. + pw_field.fill(NEW, timeout = 60_000) + page.fill("#confirm-password", NEW, timeout = 60_000) + shoot("01-change-password-filled") page.locator('button[type="submit"]').click() # ───────────────────────────────────────────────────── diff --git a/tests/studio/playwright_extra_ui.py b/tests/studio/playwright_extra_ui.py index 2bb5870552..ba32b47ee7 100644 --- a/tests/studio/playwright_extra_ui.py +++ b/tests/studio/playwright_extra_ui.py @@ -182,9 +182,17 @@ with sync_playwright() as p: # ───────────────────────────────────────────────────── step("setup: change-password + model load") page.goto(f"{BASE}/change-password") - page.locator("#new-password").wait_for(state = "visible", timeout = 60_000) - page.fill("#new-password", NEW) - page.fill("#confirm-password", NEW) + # See playwright_chat_ui.py -- wait for networkidle before + # touching the form to dodge the bootstrap-poll-induced + # rerender on slow macos-14 runners. + try: + page.wait_for_load_state("networkidle", timeout = 30_000) + except Exception: + pass + pw_field = page.locator("#new-password") + pw_field.wait_for(state = "visible", timeout = 60_000) + pw_field.fill(NEW, timeout = 60_000) + page.fill("#confirm-password", NEW, timeout = 60_000) page.locator('button[type="submit"]').click() composer = page.locator('textarea[aria-label="Message input"]') composer.wait_for(state = "visible", timeout = 60_000)