fix(cli): validate seeks the runtime player directly, not raw timelines#1895
Draft
miguel-heygen wants to merge 1 commit into
Draft
fix(cli): validate seeks the runtime player directly, not raw timelines#1895miguel-heygen wants to merge 1 commit into
miguel-heygen wants to merge 1 commit into
Conversation
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().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Two independent detailed reports converged on the same mechanism:
validate's WCAG contrast audit was flagging text inside clips that are outside theirdata-start/data-durationvisibility window, checking it against whatever background happens to be behind it in the DOM.seekTo()inpackages/cli/src/commands/validate.tsprioritizedwindow.__hf.seek()— a bridge object that only exists when the producer's render-pipeline file server injects it — before falling back to grabbingwindow.__timelinesdirectly and calling.seek()on each raw GSAP timeline.validateserves compositions through a plain static preview server that never injects that bridge, so every singlevalidaterun was silently taking the raw-timeline fallback.Seeking a raw timeline moves the animation state but skips the runtime's own visibility sync (
syncMediaForCurrentStateinpackages/core/src/runtime/init.ts), which is what setsstyle.visibility/style.displayon 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 skipsvisibility: hiddenelements but never got the chance because that style was never applied.Fix
seekTo()now preferswindow.__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. Thewindow.__hf/raw-__timelinespaths remain as fallbacks. No change needed incontrast-audit.browser.jsitself — its existing visibility filtering now sees correct computed style.Test plan
page.evaluate()callback, which Puppeteer serializes via.toString()for the browser context, so it can't import and exercise a project-localwindow.__playerstub from vitest/jsdom without testing a copy of the logic rather than the shipped code.window.__player.renderSeekis always assigned bypackages/core/src/runtime/init.ts'screatePlayerApiCompat, which forwards toplayer.renderSeek, which callssyncMediaForCurrentState()— 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 --noEmitonpackages/cli— clean.bunx oxlint/bunx oxfmt --checkon the changed file — clean.