webviews tests: frame tests read layout after the frame paints
In progress: CI running. Independent review passed; its one real finding (inline frame waits followed by reads in pr-dock and tab-row) is fixed in the second commit.
What
A requestAnimationFrame callback runs before the frame's style, layout, ResizeObserver callbacks and paint. A test that reads layout there can see a state the frame never paints, such as the transcript's last row 30 px low on an opened chat's first frame. This PR makes reading after paint the one way frame tests sample.
-
webviews/test/support/paintedFrames.ts(new):installPaintedFrames(page)defines two page globals:window.cmuxAfterPaint(read, drive?)callsread(frame)after each painted frame until it returnsfalse.drive(frame)runs in the frame's callbacks first. It returns a stop function.window.cmuxPaintFrame(drive?)resolves with the frame time once that frame has painted.
A read runs in a MessageChannel task posted from the frame callback, so it runs after the frame renders.
-
Migrated every browser test that sampled in frame callbacks: agent-pane-stream-jitter, -chat-column, -history-paging, -queue-collapse, -reading-place, -steer, -switch-frames, chat-open-perf, code-block-highlight-height, hover-row-sweep, new-tab-composer-still, new-tab-first-frame-project, new-tab-warm-first-frame, reopened-chat-history, settings-open-frames, transcript-fling, transcript-fold-anchor-browser, plus agent-pane-pr-dock and agent-pane-tab-row, which awaited two frames inside the page and then read layout.
-
Guard:
webviews/scripts/check-frame-reads.tsruns fromcheck-browser-test-lanes. It fails a browser test that does any of these inside arequestAnimationFramecallback, unless a// raf-read: <reason>comment says why:- reads layout there;
- reschedules a sampler from there;
- wraps a frame in a named awaiter (
const frame = () => new Promise(r => requestAnimationFrame(r))); - awaits a frame inline and then reads layout in the same function.
Unit tests are in
test/check-frame-reads.test.ts. -
Pending (
FRAME_READS_PENDING): these wait for other lanes' open PRs, and the guard fails once one of them reads after paint so the entry gets removed: -
New regression tests on the helper:
- A chat with rich history opens at its latest line and holds it (agent-pane-stream-jitter).
- A long chat opened at its end holds its latest line on every painted frame while history pages in above it (agent-pane-history-paging).
- Thread switching is already covered by chat-open-perf, which now samples post-paint.
Verification
- Every changed browser test ran locally on cmux16 with
CMUX_BROWSER_TESTS=1and all of them pass, except settings-open-frames. Its dev server doesn't start on cmux16, and base fails the same way here, so CI is the judge for that file. - agent-pane-chat-column failed 1 of 13 once ("a streaming reply keeps the view at its end…", resting 12 px off). It passed 13/13 on the rerun, and base passed 13/13. I'm watching it in CI.
- Guard, unit tests and
vp checkare clean.
Agent session: cmux16
🤖 Generated with Claude Code