Skip to content

fix(cli): snapshot waits for the image layers a seek reveals before capturing - #6161

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/snapshot-intermittent-layers
Oct 23, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/snapshot-intermittent-layers

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

What

hyperframes snapshot now waits, after each seek, for every visible image layer (<img>, SVG <image>, CSS background-image: url(...)) to finish decoding, for up to 3 seconds. A layer that is still loading, or broken, when the wait ends is named in a console warning and in a warnings array of the --json envelope.

Why

snapshot screenshotted as soon as the seek settled and never waited for images. A layer that appears at the seek time and is still loading (large PNGs, slow or remote assets, a cold cache) came out blank or half drawn. The frame looked complete, so anyone reading it to judge the video was sent to fix things that were fine. The failure depends on load timing, so it varies from run to run.

Related work

The 120 ms image wait in SCREENSHOT_SEEK_OPTIONS was only used by check's screenshots; snapshot passed { exactTime: true } and had no image wait at all.

How

  • captureCompositionFrame.ts: waitForCompositionImages scans visible elements for the three layer kinds and decodes each (CSS and SVG sources through an Image of the same URL), racing one cap. It returns the layers still pending or failed; settleCompositionSeek passes that on. SCREENSHOT_IMAGE_WAIT_MS (3 s) replaces the 120 ms and is shared by snapshot, check screenshots and compare. A wait only costs time while an image is loading.
  • snapshot.ts: opts in to the wait, and collects the report into warnings, printed as a warning and added to the --json envelope when non-empty.
  • Not covered: backgrounds on pseudo-elements, mask-image and border-image. check and compare wait but do not report.

Test plan

New snapshot.lateImages.browser.test.ts runs the real snapshot command against a project whose seek reveals a layer served over an 800 ms connection: one test each for an <img>, a CSS background and an SVG <image>, plus one for an image that never loads (named in warnings).

  • On main all 4 fail; with the change all 4 pass, 3 runs in a row.

  • Breaking each layer kind in the scan fails exactly its own test.

  • packages/cli: typecheck, lint and format clean; the snapshot, capture, compare and check test files pass (668 tests).

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments"

Before and after

hyperframes snapshot --at 1.5 on a project whose seek at 1 s reveals three image layers, each served after 800 ms.

Before: only a broken-image outline, the other two layers missing

After: all three layers painted

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 1 important · 0 nits · head b97262a7

The optional Comments check fails: the comment share in captureCompositionFrame.ts went from 7.1% to 7.3% (5 comment lines added). It isn't a required check, but trimming those lines turns it green.

Scope

I read every changed source file and every consumer of waitForImagesMs / SCREENSHOT_SEEK_OPTIONS: snapshot.ts, compare.ts, checkBrowser.ts:514,792. warnings is already declared at snapshot.ts:537, so the early return and the envelope are safe. I didn't run the browser test locally. CI was still running at the time of review.

const absolute = (href: string): string => new URL(href, document.baseURI).href;
for (const element of document.querySelectorAll("*")) {
const style = getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

important: this check only looks at the element's own display, and a child of a display: none ancestor keeps its own display value. So images inside a hidden container are still scanned. Example: a scene that GSAP toggles with display: none holds a large <img> that isn't in the frame at the seek time. The snapshot waits up to 3 s for it, then warns that the frame "may be missing" an image that was never supposed to be there. A loading="lazy" image in that subtree never loads, so every frame pays the full 3 s. element.checkVisibility({ visibilityProperty: true }) (Chromium 121+) covers ancestors and visibility in one call:

Suggested change
if (style.display === "none" || style.visibility === "hidden") continue;
if (!element.checkVisibility({ visibilityProperty: true })) continue;
const style = getComputedStyle(element);

If you take this, remove the const style line just above.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 138 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-center-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE — 0 resolved, 1 open (non-blocking), 0 new · b97262a7 → ee9f6934 (only removes 3 comment lines; all checks pass)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 1552f52 Oct 23, 2026
68 checks passed
@miguel-heygen
miguel-heygen deleted the fix/snapshot-intermittent-layers branch October 23, 2026 23:19
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