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.
This commit is contained in:
Daniel Han 2026-05-07 13:13:15 +00:00
commit 9bf0c6ff09
2 changed files with 35 additions and 8 deletions

View file

@ -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()
# ─────────────────────────────────────────────────────

View file

@ -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)