webviews tests: frame tests read layout after the frame paints

Merged
TE
15d ago
+574-115

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?) calls read(frame) after each painted frame until it returns false. 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.ts runs from check-browser-test-lanes. It fails a browser test that does any of these inside a requestAnimationFrame callback, 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:

    • agent-pane-scrollbar-full-height (#23257)
    • thread-minimap-feel and thread-minimap-motion (#23092); I move these in a follow-up once #23092 lands.
  • 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=1 and 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 check are clean.

Agent session: cmux16

🤖 Generated with Claude Code