@@ -243,12 +243,28 @@ test.describe("TV board phase 1 — visual board states", () => {
243243 test ( "stale — connection loss escalates to the last-known takeover" , async ( { page } ) => {
244244 // The tracker flips to `stale` after STALE_AFTER_MISSED_POLLS (15) consecutive
245245 // missed poll cycles (pinned in tests/use-display-connection.test.ts) — ≈2 min at
246- // the ~8 s failed-cycle cadence. Slow by design (cadence-aware, exit-only); the
247- // 180 s test budget + 150 s assertion timeout leave margin over the ~2 min floor.
248- test . setTimeout ( 180_000 ) ;
246+ // the ~8 s failed-cycle cadence.
247+ //
248+ // That escalation is driven purely by timers (poll interval + TanStack retry
249+ // backoff), so observing it does not require two REAL minutes of CI wall-clock:
250+ // this one test was 120 s of a 126 s shard. Playwright's clock drives those
251+ // timers instead. Measured locally: 2.0 min -> 5.5 s, same assertions.
252+ //
253+ // `install({ time })` — not a bare `install()`. A bare install freezes time
254+ // before any app script runs and `page.goto` then never settles (verified: the
255+ // navigation itself times out). Seeding a start time lets timers fire normally
256+ // during load, and only the explicit `runFor` calls below advance them after.
257+ await page . clock . install ( { time : new Date ( "2026-08-13T10:18:00.000Z" ) } ) ;
249258 let served = false ;
259+ // Counts the FAILED polls. It is the only deterministic signal that a driven tick
260+ // actually produced a request — see the loop below, which waits on it instead of on
261+ // real time.
262+ let abortedPolls = 0 ;
250263 await page . route ( "**/api/display/snapshot" , ( route ) => {
251- if ( served ) return route . abort ( "connectionrefused" ) ;
264+ if ( served ) {
265+ abortedPolls ++ ;
266+ return route . abort ( "connectionrefused" ) ;
267+ }
252268 served = true ;
253269 return route . fulfill ( {
254270 status : 200 ,
@@ -261,10 +277,36 @@ test.describe("TV board phase 1 — visual board states", () => {
261277
262278 // First (only) successful poll renders a quiet board…
263279 await expect ( page . getByTestId ( "board-state-strip" ) ) . toHaveAttribute ( "data-state" , "all_clear" ) ;
280+
264281 // …then missed polls escalate live → delayed → stale (exit-only takeover).
265- await expect ( page . getByTestId ( "board-state-strip" ) ) . toHaveAttribute ( "data-state" , "stale" , {
266- timeout : 150_000 ,
267- } ) ;
282+ // Advance one failed-cycle (~8 s) at a time, yielding the event loop between
283+ // ticks so each aborted fetch and its retry backoff actually resolve — a single
284+ // large jump collapses the cycles and the counter never advances.
285+ // The escalation lands at ~232 s of driven time (the retry backoff means a
286+ // failed cycle costs more than the bare 5 s poll interval), so 40 ticks of 8 s
287+ // leaves real margin over the observed floor without adding wall-clock cost.
288+ for ( let i = 0 ; i < 40 ; i ++ ) {
289+ // No `.catch(() => null)`: a locator timeout or a closed page is a real failure and
290+ // must surface here, at its source. Swallowing it read as "not stale yet", so the
291+ // loop span another 39 ticks and the run died 40 iterations later on the final
292+ // assertion, pointing at the wrong line.
293+ const state = await page . getByTestId ( "board-state-strip" ) . getAttribute ( "data-state" ) ;
294+ if ( state === "stale" ) break ;
295+
296+ const before = abortedPolls ;
297+ await page . clock . runFor ( 8_000 ) ;
298+ // The tick is only finished once the poll it drove has actually reached the route
299+ // handler. The previous `waitForTimeout(120)` was REAL wall-clock — a guess at how
300+ // long an aborted fetch plus its retry backoff take, and the one wall-clock
301+ // dependency left in a test whose whole purpose is not to have any. Polling the
302+ // counter is the same wait expressed as a fact instead of an estimate; it also
303+ // returns as soon as the request lands rather than always costing the full budget.
304+ await expect
305+ . poll ( ( ) => abortedPolls , { timeout : 5_000 , intervals : [ 5 , 10 , 25 , 50 ] } )
306+ . toBeGreaterThan ( before ) ;
307+ }
308+
309+ await expect ( page . getByTestId ( "board-state-strip" ) ) . toHaveAttribute ( "data-state" , "stale" ) ;
268310 const takeover = page . getByTestId ( "board-takeover" ) ;
269311 await expect ( takeover ) . toHaveAttribute ( "data-kind" , "stale" ) ;
270312 // Last-known state stays on screen, labeled — never silently zeroed.
0 commit comments