fix(test): make canvas breakpoint-frame tests wait on readiness instead of fixed sleeps#57
Merged
Conversation
The canvas-breakpoint tests flaked in CI (Verify step of the release
workflow), failing reliably on a loaded runner while passing locally.
Root cause: the canvas reveals breakpoint frames progressively
(useProgressiveCanvasFrameLoading — the active frame after a
requestAnimationFrame, each inactive frame behind a chained
setTimeout(32ms) → requestIdleCallback({timeout:160ms})), then mounts each
frame's iframe and portals the page tree in. The failing tests all query
the `mobile` frame, which — with `desktop` active — is the FIRST inactive
frame, so it appears only after that whole async chain. The tests waited
with fixed budgets: a hardcoded setTimeout(90ms) "flush" and the default
1000ms waitFor. Those are calibrated for a fast laptop; under CI
event-loop saturation the timers drift past both, so the iframe content
isn't present when the assertion runs.
Proven by inflating the reveal delay to 1500ms locally, which reproduced
the exact CI signature (activation tests timing out ~1010ms, rendering
tests failing ~110ms); with this change the same simulation passes 17/17.
Fix: replace every arbitrary sleep with condition-based waiting. Add
shared waitForCanvasFrameDocument / waitForCanvasNodeInFrame /
waitForCanvasElement helpers in iframeCanvasQuery.ts that poll the real
readiness condition with a CI-tolerant 5s ceiling (waitFor returns the
instant the condition holds, so zero cost on a fast machine). This also
removes the flushProgressiveCanvasFrames helper that was copy-pasted
across four test files. Product code is untouched — the progressive
loader is correct; the tests were making brittle timing assumptions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines
+8
to
+13
| import { | ||
| getCanvasFrameDocument, | ||
| queryCanvasNodeInFrame, | ||
| waitForCanvasFrameDocument, | ||
| waitForCanvasNodeInFrame, | ||
| } from './iframeCanvasQuery' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The canvas-breakpoint tests flaked in CI — specifically the Verify step of the release workflow (this blocked the v0.0.4 release). They failed reliably on the CI runner but passed locally, which is the tell-tale sign of a timing assumption rather than a logic bug.
Failing tests (all in
src/__tests__/canvas/):canvas breakpoint activation— 4 tests, timing out at ~1010–1048mscanvas breakpoint rendering— 3 tests, failing fast at ~16–144msRoot cause (a real, specific issue)
The canvas reveals breakpoint frames progressively (
useProgressiveCanvasFrameLoading): the active frame after arequestAnimationFrame, then each inactive frame behind a chainedsetTimeout(32ms)→requestIdleCallback({timeout:160ms}), and only then mounts each frame's iframe and portals the page tree into it.Every failing test queries the
mobileframe. Withdesktopactive (the default),mobileis the first inactive frame, so it only appears after that whole async chain. But the tests waited with fixed budgets:setTimeout(90ms)"flush" helper (copy-pasted across 4 test files), and1000mswaitFortimeout.Both are tuned for a fast laptop. Under CI event-loop saturation (this run was ~1.8× slower overall) those timers drift past the budgets, so the iframe content isn't mounted when the assertion runs.
Proven, not guessed
I reproduced the exact CI signature locally by inflating the inactive-frame reveal delay (
32ms→1500ms): the activation tests timed out at ~1010–1048ms and the rendering tests failed at ~110ms — matching CI. With this change applied, the same slow-runner simulation passes 17/17 (taking ~6.6s, i.e. the waiters correctly absorb the delay instead of giving up).Fix
Replace every arbitrary sleep with condition-based waiting:
iframeCanvasQuery.ts—waitForCanvasFrameDocument,waitForCanvasNodeInFrame,waitForCanvasElement— poll the real readiness condition with a CI-tolerant 5s ceiling.waitForreturns the instant the condition holds, so there's zero added cost on a fast machine; it only widens headroom for a slow one. The progressive reveal is bounded, so a healthy canvas always settles well within 5s and an unhealthy one fails loudly rather than hanging.flushProgressiveCanvasFrames90ms-sleep helper that was duplicated acrossbreakpointProps,canvasFormControls, andnodeRendererLockdown.No product code changes — the progressive loader is correct behaviour (it avoids mounting three heavy iframes at once); only the tests were making brittle timing assumptions.
progressiveCanvasLoading.test.tsxis intentionally left as-is: there the staggered timing is the behaviour under test.Verification
1500msslow-runner simulation that previously failed.bun test5431 pass / 0 fail;bun run buildandbun run lintclean.🤖 Generated with Claude Code