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

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

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().

* fix(cli): wait for runtime seek target in validate
This commit is contained in:
Miguel Ángel
2026-07-04 14:08:26 -07:00
committed by GitHub
parent 0338be97fd
commit 3f49f107eb
2 changed files with 68 additions and 1 deletions
+24 -1
View File
@@ -1,10 +1,11 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
extractCompositionErrorsFromLint,
navigationTimeoutHint,
raceMediaReady,
resolveNavigationTimeoutMs,
shouldIgnoreRequestFailure,
waitForPreferredSeekTarget,
} from "./validate.js";
import type { ProjectLintResult } from "../utils/lintProject.js";
@@ -91,6 +92,28 @@ describe("shouldIgnoreRequestFailure", () => {
});
});
describe("waitForPreferredSeekTarget", () => {
it("waits for the runtime player/bridge target before falling back to raw timelines", async () => {
const page = {
waitForFunction: vi.fn(async () => undefined),
};
await waitForPreferredSeekTarget(page, 123);
expect(page.waitForFunction).toHaveBeenCalledWith(expect.any(Function), { timeout: 123 });
});
it("does not fail validation when only the legacy raw timeline fallback is available", async () => {
const page = {
waitForFunction: vi.fn(async () => {
throw new Error("waiting failed: timeout");
}),
};
await expect(waitForPreferredSeekTarget(page, 1)).resolves.toBeUndefined();
});
});
describe("extractCompositionErrorsFromLint", () => {
// `bundleToSingleHtml` (the inliner validate.ts bundles through) is
// intentionally tolerant of missing/empty/unparsable data-composition-src