Skip to content

Pan to offscreen elements in video mode - #22

Open
mmkal wants to merge 13 commits into
mainfrom
video-mode-scroll-pan
Open

Pan to offscreen elements in video mode#22
mmkal wants to merge 13 commits into
mainfrom
video-mode-scroll-pan

Conversation

@mmkal

@mmkal mmkal commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

When a test waits for or clicks an element outside the viewport, the rendered video now pans to it — a smooth eased camera travel, the usual pointer/outline hold, then onward — without the test ever scrolling the page.

await page.getByText("Deploy succeeded").waitFor(); // no real scroll → video pans down, holds, pans back
await page.getByRole("button", { name: "View summary" }).click(); // real scroll → video pans down and stays

What really scrolls in Playwright, and what the video does

Operation Live page Rendered video
waitFor(), expect(...).toBeVisible() and other queries never scrolls — they only observe pan to the element, hold, pan back
click(), check(), hover() and other interactions really scrolls — actionability runs scrollIntoViewIfNeeded before acting pan to the element, hold, stay where the post-action footage resumes

Two refinements keep multi-scroll tests watchable:

  • Adjacent pans hand over instead of yo-yoing. A waitFor pan directly followed by another pan (the extremely common wait-then-click) skips its return leg; the next pan enters from the previous destination — zero travel when both point at the same view, so the hold simply continues into the action.
  • Pan destinations center the element, matching Chromium's own scroll-for-action alignment — so wait-then-act pairs land on the same view, and the held element stays clear of the caption band.

The stay-pan adopts the browser's actual scroll destination after the action, so the pan lands pixel-exact on the resumed footage — replacing what used to be an instant jump-cut. fill() keeps its reveal pipeline unpanned, and elements clipped by inner scroll containers keep plain highlighting.

How it stays true to the record-facts/render-effects philosophy: at highlight time video mode captures a beyond-viewport screenshot (fullPage + document-coordinate clip — Chromium renders offscreen pixels without scrolling: no scroll event, scroll position untouched, IntersectionObservers never fire, so lazy-loading can't trigger). The renderer animates a viewport-sized crop over that tall still with cosine-eased ffmpeg time expressions.

Known costs, handled: Chromium implements beyond-viewport capture by momentarily resizing the renderer, leaking one zoomed-out frame into the raw screencast; the pan piece consumes that span from the source so it never reaches the rendered video. The page observes a single no-op resize event with unchanged dimensions.

Also fixes a pre-existing race this work exposed: a selector-driven trimStart resolves over the protocol and could land 1–2ms after a highlight recorded at effectively the same moment, silently dropping that highlight from the render. A trim start within one source frame after a highlight start now moves back to it (the flaky text-cursor spec went from 2/3 failing to 8/8 passing).

Raw vs rendered

The checked-in demo — spec/scroll-pan-demo.spec.ts — uses the same narrow 480×720 viewport as the todo-app spec. It has targets both below the fold (success card, "View summary" button) and above it (header badge, "Copy status link" button), and asserts the live scroll position plus every recorded pan mode. The clip is caption-free so none of the page or motion is obscured.

Raw (left): waits never move; the two clicks cause instant browser scrolls. Rendered (right): waits pan smoothly, adjacent wait/click pans hand over without yo-yoing, and click pans meet the live footage at its real scroll destination. The raw stream ends first and holds its final frame while the rendered timeline completes.

scroll-pan-raw-vs-rendered.mp4

Todo-app baseline

Standard visual baseline — no pans trigger here (everything fits the viewport), pacing unchanged.

todo-app.mp4

Raw recording (recording/rendering behavior changed on this branch, per repo guidance):

todo-app-raw.mp4

Validation

108 passed / 4 provider-gated skips. After merging current main: 8 focused pan/fill-ordering checks passed, plus typecheck, build, and publint. Caption-free narrow demo: 1/1 passed. Task file: tasks/video-mode-scroll-pan.md

🤖 Generated with Claude Code

Session: c46b4c3b-ccee-4225-aa58-7d8d22fcba39


Note

Medium Risk
Large changes to video-mode recording and ffmpeg rendering paths; behavior is heavily tested but regressions in highlight timing, trim, or non-pan highlights are possible.

Overview
video mode now pans the rendered video to offscreen highlight targets using a beyond-viewport screenshot and ffmpeg crop animation, while the live page never scrolls for waitFor (and similar queries).

waitFor highlights use pan down, hold, pan back; actions like click use pan down and stay, with finalizePanHighlightAfterAction aligning the synthetic pan to Playwright’s real post-scroll position so live footage resumes without a jump-cut. fill stays unpanned; targets clipped by inner scroll containers skip panning. Adjacent pans hand over (suppress return legs) so wait-then-click does not yo-yo.

Metadata adds VideoModePan; the renderer consumes Chromium’s capture-flash span so it never appears in the output. A trimStart race fix keeps highlights that land within one frame after selector trim start. README documents behavior and limitations; spec/scroll-pan-demo.spec.ts and FFmpeg/metadata specs cover pans.

Reviewed by Cursor Bugbot for commit 50c8e66. Bugbot is set up for automated code reviews on this repo. Configure here.

Flesh out the ask 'have video mode scroll down to show elements that are
not visible'. Chosen design: never scroll in-test; capture a
beyond-viewport screenshot at highlight time and fabricate an eased
camera pan in post, consistent with video mode's record-facts/render-
effects philosophy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/middlewright@22

commit: 50c8e66

mmkal and others added 12 commits August 4, 2026 13:31
The rendered video must show a below-the-fold waitFor target (smooth
pan in, outlined hold, pan back) while the raw recording proves the
live page never scrolled. Currently the highlight records an
out-of-frame rect and the element never appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At highlight time, an offscreen target now captures a beyond-viewport
clipped screenshot (fullPage+clip; Chromium renders it without
scrolling) plus the current and destination scroll offsets. The
renderer replaces the capture span with a synthetic eased crop pan over
that tall still: travel to the element, hold the usual highlight, and
pan back to the live scroll position.

The live page never scrolls: no scroll event fires, scroll position is
unchanged, and IntersectionObservers never see the element. The one
cost is a single zoomed-out frame Chromium leaks into the screencast
while capturing beyond the viewport; the pan piece consumes that span
from the source (plus a settle margin) so it never reaches the rendered
video.

Also fixes a pre-existing race this exposed: a selector-driven trim
start resolves over the protocol and could land 1-2ms after a highlight
recorded at effectively the same moment, silently dropping that
highlight from the render. A trim start within one source frame after a
highlight start now moves back to it. The previously intermittent
text-cursor spec passes 8/8 with the clamp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playwright auto-scrolls the live page for the click, so the rendered
pan must land exactly where post-click footage resumes instead of
panning back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-action highlights (click, check, ...) on offscreen elements now pan
too. The capture extends one viewport beyond the estimated destination,
and after the action completes the pan adopts the browser's actual
scroll position, so the synthetic pan lands exactly where live footage
resumes — replacing Playwright's instant auto-scroll jump with a smooth
travel. waitFor pans return to the live scroll position; action pans
stay. Fill keeps its existing reveal pipeline unpanned.

Focused metadata specs cover the horizontal pan axis and the
inner-scroll-container fallback, where window scrolling cannot reveal
the element and plain highlighting is kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A waitFor pan followed immediately by a click pan on the same element
currently pans all the way back to the origin before panning down
again. Once the awaited element is on camera it should stay on camera.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A waitFor pan directly followed by another pan no longer travels all
the way back to the live scroll position just to pan down again: the
return leg is suppressed and the next pan enters from the previous
destination — a zero-length entry when both point at the same view, so
the hold simply continues into the action.

Also files a task for a pre-existing pointer-tail spec flake observed
identically on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Centering matches Chromium's own scroll-for-action alignment, so a
wait-then-act pan pair hands over with zero travel, and keeps the held
element clear of the caption band at the bottom of the frame. The demo
spec is now a committed public-behavior test (and the PR media
fixture): its step captions narrate which operations really scroll the
live page and which only pan the video.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the todo-app baseline's 480x720 viewport so raw and rendered recordings can be compared at useful scale in one PR video. Document the review-media follow-up in the task.
Resolve the video-mode overlap by keeping main's unbounded still inputs for chronological fill-frame rendering while retaining the scroll-pan metadata, filters, and coverage. Also adopt main's readable Playwright fixture naming.
Flatten the demo's test.step wrappers while keeping its live-scroll and pan-mode assertions. The checked-in PR media fixture now renders without captions obscuring the narrow side-by-side view.
@mmkal
mmkal marked this pull request as ready for review August 4, 2026 20:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 50c8e66. Configure here.

Comment thread src/plugins/video-mode.ts
...options.highlight.rect,
x: documentRect.x - scroll.x,
y: documentRect.y - scroll.y,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stay pan breaks when scroll unchanged

Medium Severity

finalizePanHighlightAfterAction always adopts the post-action scroll, but when the action does not move the page (blur, click({ force: true }), or a handler that scrolls back), pan.to collapses onto pan.from. panDurationMs still enforces PAN_MIN_MS, and the hold rect is rewritten to offscreen coordinates, so the render inserts a idle pan and an invisible highlight before live footage resumes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 50c8e66. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant