CI(ui): nuke startViewTransition + force=True nav clicks (Chromium reliability)

Chat UI Tests was failing in CI with "<html> intercepts pointer events"
on the New Chat sidebar click. Root cause: after the theme toggle's
animated reveal, Chromium's view-transition state can leave the html
element reported as the topmost click target for a beat -- even after
the documentElement classList has settled. The previous CSS-only
neutraliser (animation: none + pointer-events: auto) wasn't enough
once the runtime captured the html.

Two-pronged fix in both playwright_chat_ui.py and playwright_extra_ui.py:

  1. Monkey-patch document.startViewTransition in add_init_script so
     the callback runs synchronously, no animation pipeline runs, and
     the html is never captured. This is the only way to fully
     neutralise the transition without disabling the feature in the
     app code.
  2. Use force=True + a 5s timeout in click_nav() (sidebar nav
     clicks). The element IS visible + enabled; force=True bypasses
     Playwright's actionability check belt-and-suspenders if the
     monkey-patch ever misses an edge case.

Also broadened the CSS pseudo-element list (added ::view-transition,
-group, -image-pair) to display:none, so even if startViewTransition
is somehow re-attached, the captured pseudos can't paint over the page.
This commit is contained in:
Daniel Han 2026-05-07 05:38:54 +00:00
commit 4aff5610e5
2 changed files with 67 additions and 6 deletions

View file

@ -64,6 +64,13 @@ ART.mkdir(parents = True, exist_ok = True)
# a partial Studio install.
STRICT = os.environ.get("STUDIO_UI_STRICT", "0") == "1"
# Per-turn assistant-bubble wait. The free macos-14 runner (3 vCPU /
# 7 GB / no GPU) is ~3-5x slower at gemma-3-270m CPU inference than the
# free ubuntu-latest runner; "Say the word 'tree'" has been observed to
# hit the 180 s default exactly. STUDIO_UI_TURN_TIMEOUT_MS lets the Mac
# CI bump this without hard-coding a Mac branch in the test.
TURN_TIMEOUT_MS = int(os.environ.get("STUDIO_UI_TURN_TIMEOUT_MS", "180000"))
_n = [0]
@ -128,10 +135,22 @@ with sync_playwright() as p:
ctx.add_init_script("""
(function () {
try {
// Nuke view-transition pseudo-elements completely +
// monkey-patch document.startViewTransition so the
// theme toggle's animated reveal never runs in CI.
// Without this, the post-transition html element keeps
// intercepting pointer events on later clicks (sidebar
// nav, account menu, etc.) -- Playwright surfaces this
// as "<html> intercepts pointer events".
const css = `
::view-transition,
::view-transition-group(*),
::view-transition-image-pair(*),
::view-transition-old(*),
::view-transition-new(*) {
display: none !important;
animation: none !important;
opacity: 0 !important;
}
html, body { pointer-events: auto !important; }
`;
@ -139,6 +158,20 @@ with sync_playwright() as p:
style.id = "playwright-no-view-transition";
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
// Replace startViewTransition with an immediate-call
// shim so the callback runs synchronously without the
// animation pipeline ever capturing the html element.
if (typeof document.startViewTransition === "function") {
document.startViewTransition = function (cb) {
try { if (cb) cb(); } catch (e) {}
return {
ready: Promise.resolve(),
finished: Promise.resolve(),
updateCallbackDone: Promise.resolve(),
skipTransition: () => {},
};
};
}
} catch (e) { /* noop */ }
})();
""")
@ -383,7 +416,7 @@ with sync_playwright() as p:
return nonEmpty >= want;
}""",
arg = want_assistants,
timeout = 180_000,
timeout = TURN_TIMEOUT_MS,
)
try:
page.wait_for_selector(
@ -661,11 +694,16 @@ with sync_playwright() as p:
if btn is None:
soft_fail(f"nav '{label}' not found")
return False
# force=True bypasses Playwright's actionability check. The
# button IS visible + enabled, but the post-theme-toggle view-
# transition can leave <html> reported as the topmost element
# for a beat (we already neutralise startViewTransition via
# add_init_script; this is belt-and-suspenders).
try:
btn.scroll_into_view_if_needed(timeout = 2000)
except Exception:
pass
btn.click()
btn.click(force = True, timeout = 5_000)
except Exception as exc:
soft_fail(f"nav '{label}' click failed: {exc!r}")
return False
page.wait_for_timeout(800)
if expected_url_pat and not re.search(expected_url_pat, page.url):
soft_fail(

View file

@ -76,13 +76,36 @@ with sync_playwright() as p:
ctx.add_init_script("""
(function () {
try {
// Same shim as playwright_chat_ui.py: nuke view-
// transition pseudo-elements + monkey-patch
// startViewTransition so the html element never gets
// captured (which Playwright surfaces as "<html>
// intercepts pointer events" on later clicks).
const style = document.createElement("style");
style.textContent = `
::view-transition,
::view-transition-group(*),
::view-transition-image-pair(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
::view-transition-new(*) {
display: none !important;
animation: none !important;
opacity: 0 !important;
}
html, body { pointer-events: auto !important; }
`;
(document.head || document.documentElement).appendChild(style);
if (typeof document.startViewTransition === "function") {
document.startViewTransition = function (cb) {
try { if (cb) cb(); } catch (e) {}
return {
ready: Promise.resolve(),
finished: Promise.resolve(),
updateCallbackDone: Promise.resolve(),
skipTransition: () => {},
};
};
}
} catch (e) {}
})();
""")