From 2b7eb4cdd195455f507d3458d90d7712d65cb826 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Wed, 19 Aug 2026 17:17:36 +0000 Subject: [PATCH] tests: fix rAF-stall flakes in headless chromium; login form submits on Enter Headless Chromium 151 stops generating animation frames on idle pages, which stalls Playwright's rAF-based waits (wait_for_function default polling, click actionability 'stable' check). Fixes: - wait_for_function: explicit timer polling (100ms/250ms) - video readyState wait: 120s (cold upstream/CDN starts) - .card click: force=True - login: form submit handler (Enter key works, was button-only) - test asserts the seeded entry, not a section header that only appeared via an API-test side effect --- static/login.html | 6 +++--- tests/test_browser.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/static/login.html b/static/login.html index d199174..5028991 100644 --- a/static/login.html +++ b/static/login.html @@ -19,7 +19,7 @@ button { width:100%; margin-top:.4rem; padding:.55rem; border:0; border-radius:8 -
+

ani-web

@@ -35,7 +35,7 @@ const BASE = (() => { })(); const $ = id => document.getElementById(id); const next = new URLSearchParams(location.search).get("next"); -$("go").onclick = async () => { +async function login() { $("err").textContent = ""; const r = await fetch(BASE + "login", {method: "POST", headers: {"Content-Type": "application/json"}, @@ -45,7 +45,7 @@ $("go").onclick = async () => { } else { $("err").textContent = (await r.json().catch(() => ({}))).detail || "login failed"; } -}; +} diff --git a/tests/test_browser.py b/tests/test_browser.py index 6e8c159..ab72f11 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -11,6 +11,10 @@ from playwright.async_api import async_playwright BASE = "http://127.0.0.1:8399" LAUNCH_ARGS = ["--autoplay-policy=no-user-gesture-required"] +# This headless Chromium stops generating animation frames on idle pages, +# which stalls rAF-based Playwright waits (wait_for_function's default +# polling, click actionability). Poll by timer instead; force the one click. +POLL = 100 async def _login(page, user, pw): @@ -19,10 +23,14 @@ async def _login(page, user, pw): assert "create account" not in await page.content(), "self-signup UI must be absent" await page.fill("#user", user) await page.fill("#pw", pw) - await page.click("#go") + # Enter instead of clicking #go: this playwright/chromium combo's + # "element stable" actionability check hangs non-deterministically; + # fill() leaves focus on the field, so a bare keypress submits. + await page.keyboard.press("Enter") await page.wait_for_selector("#q", timeout=15000) await page.wait_for_function( - f"document.getElementById('me').textContent.includes('{user}')", timeout=5000) + f"document.getElementById('me').textContent.includes('{user}')", + timeout=5000, polling=POLL) async def test_login_ui(): @@ -40,14 +48,14 @@ async def test_login_ui(): # wrong password -> inline error await page.fill("#user", "admin") await page.fill("#pw", "wrong") - await page.click("#go") + await page.keyboard.press("Enter") await page.wait_for_function("document.querySelector('#err').textContent.length > 0", - timeout=10000) + timeout=10000, polling=POLL) assert "wrong" in await page.inner_text("#err") # correct -> home with user chip await page.fill("#pw", "adminpw12345") - await page.click("#go") + await page.keyboard.press("Enter") await _login_settled(page) assert await page.inner_text("#me") == "hi, admin" assert "Seed Anime" in await page.inner_text("#main"), "seeded history must show" @@ -59,7 +67,7 @@ async def test_login_ui(): async def _login_settled(page): await page.wait_for_selector("#q", timeout=15000) await page.wait_for_function( - "document.getElementById('me').textContent.length > 0", timeout=5000) + "document.getElementById('me').textContent.length > 0", timeout=5000, polling=POLL) async def test_video_plays_behind_auth(): @@ -74,11 +82,11 @@ async def test_video_plays_behind_auth(): await page.fill("#q", "chainsaw") await page.keyboard.press("Enter") await page.wait_for_selector(".card", timeout=30000) - await page.click(".card") + await page.click(".card", force=True) await page.wait_for_selector("video", timeout=30000) await page.wait_for_function( "document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1", - timeout=60000) + timeout=120000, polling=250) # 120s: cold CDN/upstream starts can be slow real_errors = [e for e in errors if "401" not in e] assert not real_errors, real_errors await browser.close()