Skip to content

fix(cli): validate seeks the runtime player directly, not raw timelines#1895

Draft
miguel-heygen wants to merge 1 commit into
mainfrom
fix/validate-seek-visibility-sync
Draft

fix(cli): validate seeks the runtime player directly, not raw timelines#1895
miguel-heygen wants to merge 1 commit into
mainfrom
fix/validate-seek-visibility-sync

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Root cause

Two independent detailed reports converged on the same mechanism: validate's WCAG contrast audit was flagging text inside clips that are outside their data-start/data-duration visibility window, checking it against whatever background happens to be behind it in the DOM.

seekTo() in packages/cli/src/commands/validate.ts prioritized window.__hf.seek() — a bridge object that only exists when the producer's render-pipeline file server injects it — before falling back to grabbing window.__timelines directly and calling .seek() on each raw GSAP timeline. validate serves compositions through a plain static preview server that never injects that bridge, so every single validate run was silently taking the raw-timeline fallback.

Seeking a raw timeline moves the animation state but skips the runtime's own visibility sync (syncMediaForCurrentState in packages/core/src/runtime/init.ts), which is what sets style.visibility/style.display on clips currently outside their timeline window. Skipping it left off-window elements looking fully visible to any check that reads computed style afterward — in this case the contrast audit (contrast-audit.browser.js), which already correctly skips visibility: hidden elements but never got the chance because that style was never applied.

Fix

seekTo() now prefers window.__player.renderSeek, which the composition runtime exposes directly on every page load (no bridge script required) and which runs the same visibility sync used by the real render/capture pipeline. The window.__hf/raw-__timelines paths remain as fallbacks. No change needed in contrast-audit.browser.js itself — its existing visibility filtering now sees correct computed style.

Test plan

  • Added no new automated test: the branch selection runs entirely inside a page.evaluate() callback, which Puppeteer serializes via .toString() for the browser context, so it can't import and exercise a project-local window.__player stub from vitest/jsdom without testing a copy of the logic rather than the shipped code.
  • Verified the fix is correct by reading the runtime chain end-to-end instead: window.__player.renderSeek is always assigned by packages/core/src/runtime/init.ts's createPlayerApiCompat, which forwards to player.renderSeek, which calls syncMediaForCurrentState() — the exact function that walks [data-start] elements and sets their inline visibility per the current time.
  • bunx vitest run packages/cli/src/commands/validate.test.ts — 10/10 passing (unchanged, confirms no regression).
  • bunx tsc --noEmit on packages/cli — clean.
  • bunx oxlint / bunx oxfmt --check on the changed file — clean.

validate's seekTo() only checked for window.__hf.seek (a bridge object
the producer's render-pipeline file server injects) before falling
back to grabbing window.__timelines and calling .seek() on each raw
GSAP timeline directly. validate serves compositions through a plain
static file server that never injects that bridge, so this fallback
ran on every single validate invocation.

Seeking a raw timeline moves the animation state but skips the
runtime's own [data-start]/[data-duration] visibility sync
(syncMediaForCurrentState in packages/core/src/runtime/init.ts), which
is what sets an off-window clip's inline visibility/display styles.
Skipping it left elements outside their timeline window looking fully
visible to any check that reads computed style afterward at that seek
time.

This surfaced as validate's WCAG contrast audit
(contrast-audit.browser.js) flagging text in off-window clips against
whatever background happened to be behind them, since its own
visibility filtering trusts the runtime to have already hidden them.

Fix: prefer window.__player.renderSeek, which the composition runtime
exposes directly on every page load (no bridge required) and which
does run the visibility sync, before falling back to the __hf/raw
timeline paths. No changes needed to contrast-audit.browser.js itself
since its existing visibility check now sees correct computed style.

No new test added: seekTo's branch selection runs entirely inside a
page.evaluate() callback, which Puppeteer serializes via .toString()
for the browser context, so it can't import and call a project-local
window.__player stub from a jsdom/vitest test without testing a copy
of the logic rather than the shipped code. Verified instead by reading
the runtime chain end-to-end: window.__player.renderSeek is always set
by packages/core/src/runtime/init.ts's createPlayerApiCompat, calls
through to player.renderSeek, which calls syncMediaForCurrentState().
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