Studio UI test: recover from voice-picker renderer crash, scoped to macOS runners
Downgrades a headless-Chromium renderer crash in the voice model-picker step to a warning plus page recovery on macos-14, where CheckMediaAccessPermission can kill the tab. Linux and Windows strict smoke jobs keep hard crash coverage and any live-page failure stays a hard fail.
This commit is contained in:
parent
a7761e1740
commit
707b74fac3
1 changed files with 46 additions and 11 deletions
|
|
@ -36,6 +36,9 @@ ART_DIR = os.environ.get("PW_ART_DIR", "logs/playwright_extra")
|
|||
ART = Path(ART_DIR)
|
||||
ART.mkdir(parents = True, exist_ok = True)
|
||||
STRICT = os.environ.get("STUDIO_UI_STRICT", "0") == "1"
|
||||
# The Voice-picker media-access crash is specific to headless Chromium on macos-14; only there do we
|
||||
# downgrade a renderer crash to a warning. Linux/Windows strict smoke jobs keep hard crash coverage.
|
||||
MACOS_RUNNER = os.environ.get("RUNNER_OS", "").lower() == "macos" or sys.platform == "darwin"
|
||||
# Longer turn timeout: gemma-3-270m CPU inference is 3-5x slower on macos-14 runners.
|
||||
TURN_TIMEOUT_MS = int(os.environ.get("STUDIO_UI_TURN_TIMEOUT_MS", "180000"))
|
||||
WALL_TIMEOUT_S = float(os.environ.get("STUDIO_UI_WALL_TIMEOUT_S", "720"))
|
||||
|
|
@ -71,6 +74,18 @@ def runtime_warn(m: str) -> None:
|
|||
info(f"WARN (runtime): {m}")
|
||||
|
||||
|
||||
def page_crashed(pg, exc: Exception) -> bool:
|
||||
"""True when the browser/page/context died (a macos-14 renderer crash) rather than a live-page
|
||||
assertion failing -- so the caller can downgrade CI-environment flakiness to a runtime warning."""
|
||||
try:
|
||||
if pg.is_closed():
|
||||
return True
|
||||
except Exception:
|
||||
return True
|
||||
msg = str(exc).lower()
|
||||
return "has been closed" in msg or "target closed" in msg or "crash" in msg
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
_watchdog = install_wall_clock_watchdog(
|
||||
WALL_TIMEOUT_S,
|
||||
|
|
@ -544,13 +559,17 @@ with sync_playwright() as p:
|
|||
if voice_tab.count() == 0:
|
||||
fail("Voice settings tab not found")
|
||||
else:
|
||||
voice_tab.click()
|
||||
page.get_by_label("Dictation engine").click()
|
||||
page.get_by_role("option", name = "Local transcription").click()
|
||||
page.get_by_label("Speech recognition model").click()
|
||||
page.get_by_placeholder("Search model").fill("whisper")
|
||||
results = page.get_by_test_id("stt-model-results")
|
||||
# The dictation-engine dropdown touches a media-access path that can crash headless
|
||||
# Chromium on macos-14 (CheckMediaAccessPermission). A resulting TargetClosedError is CI
|
||||
# flakiness there, not a product bug, so on macOS a crash is a runtime warning + page
|
||||
# recovery; on Linux/Windows a crash and any live-page failure stay a hard fail.
|
||||
try:
|
||||
voice_tab.click()
|
||||
page.get_by_label("Dictation engine").click()
|
||||
page.get_by_role("option", name = "Local transcription").click()
|
||||
page.get_by_label("Speech recognition model").click()
|
||||
page.get_by_placeholder("Search model").fill("whisper")
|
||||
results = page.get_by_test_id("stt-model-results")
|
||||
page.wait_for_function(
|
||||
"""() => {
|
||||
const node = document.querySelector('[data-testid="stt-model-results"]');
|
||||
|
|
@ -569,10 +588,23 @@ with sync_playwright() as p:
|
|||
)
|
||||
info("OK Voice model picker mouse wheel changed scrollTop")
|
||||
except Exception as exc:
|
||||
fail(f"Voice model picker did not wheel-scroll: {exc!r}")
|
||||
shoot("10-settings-tabs-visited")
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(300)
|
||||
if page_crashed(page, exc) and MACOS_RUNNER:
|
||||
runtime_warn(f"Voice model picker aborted (browser/page unstable): {exc!r}")
|
||||
page = recover_or_replace_page(
|
||||
page,
|
||||
ctx,
|
||||
default_timeout_ms = 60_000,
|
||||
info = lambda m: info(f"recovery: {m}"),
|
||||
)
|
||||
else:
|
||||
fail(f"Voice model picker did not wheel-scroll: {exc!r}")
|
||||
# When the crash closed the context/browser (not just the page), recover_or_replace_page
|
||||
# cannot mint a replacement and hands back the closed page; skip the cosmetic teardown rather
|
||||
# than re-raise TargetClosedError on it. is_closed() is a local check and never raises.
|
||||
if not page.is_closed():
|
||||
shoot("10-settings-tabs-visited")
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(300)
|
||||
info(f"visited Settings tabs: {seen_tabs}")
|
||||
if not seen_tabs:
|
||||
soft_fail("no Settings tabs were visitable")
|
||||
|
|
@ -591,4 +623,7 @@ with sync_playwright() as p:
|
|||
sys.exit(1)
|
||||
info("PASS extra UI flow")
|
||||
_watchdog.cancel()
|
||||
browser.close()
|
||||
try:
|
||||
browser.close()
|
||||
except Exception:
|
||||
pass # a crashed browser may already be gone; never fail teardown after PASS
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue