From a108ffa1f2451503dd64a1e5bde01d4dbf5e8286 Mon Sep 17 00:00:00 2001 From: xuanru Date: Tue, 7 Jul 2026 04:01:29 +0000 Subject: [PATCH] feat(validate): extract @hyperframes/validate importable runtime validate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser-based `validate` (console/network/HTTP + contrast/clip audits, remote-asset localization, viewport-from-comp) only existed inline in the `validate` CLI command, so external consumers couldn't reuse it — they reimplement a parallel headless harness that drifts from render (missing localize → false CORS; hardcoded viewport → false out-of-frame; hand-maintained runtime parity). Lint doesn't have this problem: it's an importable `@hyperframes/lint` package. This lifts the browser core into a new `@hyperframes/validate` package, exporting `validateHtmlInBrowser(html, opts)`. The CLI `validate` command is behavior-unchanged: it lints + bundles, resolves Chrome via its own `ensureBrowser()`, and delegates. The package takes `opts.browserExecutablePath` so the caller owns browser discovery — the package stays launch-agnostic (no browser/manager), like lint owns rules but not I/O. New `opts.onPage(page)` runs extra checks on the SAME loaded page before teardown (findings merged), so a consumer can layer its own gates (e.g. caption-zone / out-of-frame) without a second browser load. - contrast-audit.browser.js moves to the package (its only consumer); the CLI build copies it into dist and the bundled runtime loads it via the same page.addScriptTag path. - The small low-level helpers validate needs (static file server, comp-viewport parse, linkedom shim, error normalizer) are vendored into the package so it doesn't import from @hyperframes/cli (which depends on it); folding both onto a shared low-level package is a follow-up. Marked in .fallowrc duplicates.ignore, matching the repo's moved-code convention. - Pure-function + contrast-script tests move to the package with the code they cover; the CLI keeps only extractCompositionErrorsFromLint (a CLI-side lint→error shaping concern). Verified: package build + typecheck clean, package tests 13/13, CLI tests 1270/1270 (103 files), fallow audit clean on 20 changed files, and an e2e `hyperframes validate` on a real composition through the package (Chrome launch + contrast audit) returns ok:true. Co-Authored-By: Claude Opus 4.8 --- .fallowrc.jsonc | 12 +- bun.lock | 54 +- package.json | 2 +- packages/cli/package.json | 1 + packages/cli/scripts/build-copy.mjs | 3 +- .../src/commands/layout-audit.browser.test.ts | 75 --- packages/cli/src/commands/validate.test.ts | 171 +----- packages/cli/src/commands/validate.ts | 459 +--------------- packages/cli/vitest.config.ts | 7 + packages/validate/package.json | 58 ++ packages/validate/src/browserValidate.test.ts | 149 +++++ packages/validate/src/browserValidate.ts | 507 ++++++++++++++++++ packages/validate/src/compositionViewport.ts | 36 ++ .../src}/contrast-audit.browser.js | 0 .../src/contrast-audit.browser.test.ts | 105 ++++ packages/validate/src/dom.ts | 11 + packages/validate/src/errorMessage.ts | 18 + packages/validate/src/index.ts | 6 + packages/validate/src/staticProjectServer.ts | 123 +++++ packages/validate/tsconfig.json | 18 + packages/validate/tsup.config.ts | 14 + 21 files changed, 1122 insertions(+), 707 deletions(-) create mode 100644 packages/validate/package.json create mode 100644 packages/validate/src/browserValidate.test.ts create mode 100644 packages/validate/src/browserValidate.ts create mode 100644 packages/validate/src/compositionViewport.ts rename packages/{cli/src/commands => validate/src}/contrast-audit.browser.js (100%) create mode 100644 packages/validate/src/contrast-audit.browser.test.ts create mode 100644 packages/validate/src/dom.ts create mode 100644 packages/validate/src/errorMessage.ts create mode 100644 packages/validate/src/index.ts create mode 100644 packages/validate/src/staticProjectServer.ts create mode 100644 packages/validate/tsconfig.json create mode 100644 packages/validate/tsup.config.ts diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 480ef20940..df303ad2d1 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -26,8 +26,9 @@ // page.addScriptTag (see layout.ts / validate.ts) — referenced by file // path, never imported, so they have no import-graph referrer. "packages/cli/src/commands/layout-audit.browser.js", - "packages/cli/src/commands/contrast-audit.browser.js", "packages/cli/src/commands/motion-sample.browser.js", + // @hyperframes/validate contrast audit injects it via page.addScriptTag (raw string, no import referrer). + "packages/validate/src/contrast-audit.browser.js", // Worker entry points loaded dynamically by their *Pool.ts companions. "packages/producer/src/services/pngDecodeBlitWorker.ts", "packages/producer/src/services/shaderTransitionWorker.ts", @@ -305,6 +306,13 @@ // new package makes fallow see it as a fresh finding; the underlying clone // predates this refactor. "packages/parsers/src/hfIds.ts", + // @hyperframes/validate vendors these from cli/src/utils/* so it needn't import @hyperframes/cli (which depends on it); a shared low-level package is a follow-up. + "packages/validate/src/staticProjectServer.ts", + "packages/validate/src/compositionViewport.ts", + "packages/validate/src/dom.ts", + "packages/validate/src/errorMessage.ts", + // browserValidate.ts: raceMediaReady vs auditClipDurations' in-page closure share the race wiring; intentional — the closure is Puppeteer-serialized into a separate realm (see raceMediaReady doc). + "packages/validate/src/browserValidate.ts", // Parser test files: parallel arrange/act/assert test cases — pre-existing // duplication moved from packages/core/src/parsers/. "packages/parsers/src/gsapParser.test.ts", @@ -573,6 +581,8 @@ "packages/studio/src/player/components/TimelineCanvas.tsx", "packages/studio/src/player/components/TimelineClip.tsx", "packages/studio/src/player/components/TimelineClipDiamonds.tsx", + // WCAG contrast sampler ported wholesale into @hyperframes/validate; high cyclomatic is inherent to the pixel scan (no new logic). + "packages/validate/src/contrast-audit.browser.js", ], }, } diff --git a/bun.lock b/bun.lock index cac1f849d6..c95cdb9c9d 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/aws-lambda": { "name": "@hyperframes/aws-lambda", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-sfn": "^3.700.0", @@ -54,7 +54,7 @@ }, "packages/cli": { "name": "@hyperframes/cli", - "version": "0.7.22", + "version": "0.7.38", "bin": { "hyperframes": "./dist/cli.js", }, @@ -86,6 +86,7 @@ "@hyperframes/producer": "workspace:*", "@hyperframes/studio": "workspace:*", "@hyperframes/studio-server": "workspace:*", + "@hyperframes/validate": "workspace:*", "@types/adm-zip": "^0.5.7", "@types/fontkit": "^2.0.9", "@types/mime-types": "^3.0.1", @@ -103,7 +104,7 @@ }, "packages/core": { "name": "@hyperframes/core", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@chenglou/pretext": "^0.0.5", "@hyperframes/lint": "workspace:*", @@ -128,7 +129,7 @@ }, "packages/engine": { "name": "@hyperframes/engine", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@hono/node-server": "^1.13.0", "@hyperframes/core": "workspace:^", @@ -146,7 +147,7 @@ }, "packages/gcp-cloud-run": { "name": "@hyperframes/gcp-cloud-run", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@google-cloud/storage": "^7.14.0", "@google-cloud/workflows": "^4.2.0", @@ -166,7 +167,7 @@ }, "packages/lint": { "name": "@hyperframes/lint", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@hyperframes/parsers": "workspace:*", "linkedom": "^0.18.12", @@ -182,7 +183,7 @@ }, "packages/parsers": { "name": "@hyperframes/parsers", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@babel/parser": "^7.27.0", "acorn": "^8.17.0", @@ -202,7 +203,7 @@ }, "packages/player": { "name": "@hyperframes/player", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@hyperframes/core": "workspace:*", }, @@ -217,7 +218,7 @@ }, "packages/producer": { "name": "@hyperframes/producer", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@fontsource/archivo-black": "^5.2.8", "@fontsource/eb-garamond": "^5.2.7", @@ -261,7 +262,7 @@ }, "packages/sdk": { "name": "@hyperframes/sdk", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -288,7 +289,7 @@ }, "packages/shader-transitions": { "name": "@hyperframes/shader-transitions", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "html2canvas": "^1.4.1", }, @@ -300,7 +301,7 @@ }, "packages/studio": { "name": "@hyperframes/studio", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", @@ -348,7 +349,7 @@ }, "packages/studio-server": { "name": "@hyperframes/studio-server", - "version": "0.7.22", + "version": "0.7.38", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -365,6 +366,25 @@ "vitest": "^3.2.4", }, }, + "packages/validate": { + "name": "@hyperframes/validate", + "version": "0.7.38", + "dependencies": { + "@hyperframes/core": "workspace:*", + "@hyperframes/engine": "workspace:*", + "@hyperframes/producer": "workspace:*", + "linkedom": "^0.18.12", + "puppeteer-core": "24.40.0", + }, + "devDependencies": { + "@types/node": "^25.0.10", + "happy-dom": "^20.9.0", + "tsup": "^8.0.0", + "tsx": "^4.21.0", + "typescript": "^5.0.0", + "vitest": "^3.2.4", + }, + }, }, "overrides": { "@types/react": "^19.0.0", @@ -741,6 +761,8 @@ "@hyperframes/studio-server": ["@hyperframes/studio-server@workspace:packages/studio-server"], + "@hyperframes/validate": ["@hyperframes/validate@workspace:packages/validate"], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -2177,6 +2199,8 @@ "@google-cloud/storage/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], + "@hyperframes/validate/puppeteer-core": ["puppeteer-core@24.40.0", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1581282", "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -2295,6 +2319,10 @@ "@google-cloud/storage/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], + "@hyperframes/validate/puppeteer-core/@puppeteer/browsers": ["@puppeteer/browsers@2.13.0", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA=="], + + "@hyperframes/validate/puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1581282", "", {}, "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/package.json b/package.json index 33b4b16ce2..b998490327 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "type": "module", "scripts": { "dev": "bun run studio", - "build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build", + "build": "bun run --filter '@hyperframes/{parsers,lint,studio-server}' build && bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/validate build && bun run --filter @hyperframes/cli build", "build:producer": "bun run --filter @hyperframes/producer build", "studio": "bun run --filter @hyperframes/studio dev", "build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime", diff --git a/packages/cli/package.json b/packages/cli/package.json index d907879201..7b2dc37457 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,6 +54,7 @@ "@hyperframes/producer": "workspace:*", "@hyperframes/studio": "workspace:*", "@hyperframes/studio-server": "workspace:*", + "@hyperframes/validate": "workspace:*", "@types/adm-zip": "^0.5.7", "@types/fontkit": "^2.0.9", "@types/mime-types": "^3.0.1", diff --git a/packages/cli/scripts/build-copy.mjs b/packages/cli/scripts/build-copy.mjs index 55f198e91a..11d5f7355e 100644 --- a/packages/cli/scripts/build-copy.mjs +++ b/packages/cli/scripts/build-copy.mjs @@ -99,7 +99,8 @@ async function main() { cpSync(layoutAuditScript, join(DIST, "commands", "layout-audit.browser.js")); } - const contrastAuditScript = join(CLI_ROOT, "src", "commands", "contrast-audit.browser.js"); + // Owned by @hyperframes/validate; bundled CLI loads it from dist/commands via page.addScriptTag. + const contrastAuditScript = join(CLI_ROOT, "..", "validate", "src", "contrast-audit.browser.js"); if (existsSync(contrastAuditScript)) { cpSync(contrastAuditScript, join(DIST, "commands", "contrast-audit.browser.js")); } diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index e084637f29..bea24800ab 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -6,7 +6,6 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const script = readFileSync(join(__dirname, "layout-audit.browser.js"), "utf-8"); -const contrastScript = readFileSync(join(__dirname, "contrast-audit.browser.js"), "utf-8"); interface RectInput { left: number; @@ -196,47 +195,6 @@ describe("layout-audit.browser content overlap", () => { }); }); -describe("contrast-audit.browser clip-path visibility", () => { - afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllGlobals(); - document.body.innerHTML = ""; - delete (document as unknown as { elementFromPoint?: unknown }).elementFromPoint; - delete (window as unknown as { __contrastAudit?: unknown }).__contrastAudit; - }); - - it("excludes text clipped to nothing by clip-path from contrast reports", async () => { - document.body.innerHTML = ` -
-
Hidden text
-
- `; - - vi.spyOn(window, "getComputedStyle").mockImplementation((element) => { - const id = (element as Element).id; - return { - display: "block", - visibility: "visible", - opacity: "1", - color: "rgb(0, 0, 0)", - fontSize: "32px", - fontWeight: "400", - clipPath: id === "headline" ? "inset(0px 100% 0px 0px)" : "none", - } as unknown as CSSStyleDeclaration; - }); - - vi.spyOn(document.getElementById("headline")!, "getBoundingClientRect").mockReturnValue( - rect({ left: 100, top: 100, width: 400, height: 80 }), - ); - (document as unknown as { elementFromPoint: () => Element | null }).elementFromPoint = () => - null; - - installContrastScript(); - - expect(await runContrastAudit()).toEqual([]); - }); -}); - // Both blocks overlap heavily; only the exemption on block A should suppress // the finding, so a missing exemption would surface as a failure here. function expectExemptFromOverlap(aOverrides: { color?: string; attrs?: string }): void { @@ -446,39 +404,6 @@ function installAuditScript(): void { window.eval(script); } -function installContrastScript(): void { - class MockImage { - onload: (() => void) | null = null; - onerror: (() => void) | null = null; - naturalWidth = 640; - naturalHeight = 360; - - set src(_value: string) { - this.onload?.(); - } - } - - vi.stubGlobal("Image", MockImage); - const getContextSpy = vi.spyOn(HTMLCanvasElement.prototype, "getContext") as unknown as { - mockReturnValue(value: CanvasRenderingContext2D): void; - }; - getContextSpy.mockReturnValue({ - drawImage() {}, - getImageData() { - return { data: new Uint8ClampedArray(640 * 360 * 4).fill(255) }; - }, - } as unknown as CanvasRenderingContext2D); - window.eval(contrastScript); -} - -async function runContrastAudit(): Promise>> { - return ( - window as unknown as { - __contrastAudit: (imgBase64: string, time: number) => Promise>>; - } - ).__contrastAudit("stub", 0); -} - function runAudit(): Array<{ code: string; selector: string; diff --git a/packages/cli/src/commands/validate.test.ts b/packages/cli/src/commands/validate.test.ts index 7353806c41..022b208a70 100644 --- a/packages/cli/src/commands/validate.test.ts +++ b/packages/cli/src/commands/validate.test.ts @@ -1,138 +1,7 @@ -import { describe, expect, it, vi } from "vitest"; -import { - extractCompositionErrorsFromLint, - navigationTimeoutHint, - raceMediaReady, - resolveNavigationTimeoutMs, - shouldIgnoreRequestFailure, - waitForPreferredSeekTarget, -} from "./validate.js"; +import { describe, expect, it } from "vitest"; +import { extractCompositionErrorsFromLint } from "./validate.js"; import type { ProjectLintResult } from "../utils/lintProject.js"; -// validateInBrowser lazy-loads the producer localize helpers via loadProducer; -// mock it so these unit tests never resolve @hyperframes/producer's built dist. -vi.mock("../utils/producer.js", () => ({ - loadProducer: vi.fn(async () => ({ - localizeRemoteMediaSources: vi.fn(async (html: string) => ({ - html, - remoteMediaAssets: new Map(), - })), - localizeRemoteImageSources: vi.fn(async (html: string) => ({ - html, - remoteMediaAssets: new Map(), - })), - localizeRemoteFontFaces: vi.fn(async (html: string) => ({ - html, - remoteMediaAssets: new Map(), - })), - })), -})); - -// Regression for the validate audio-duration-probe timeout: a slow-loading -// media element's duration was snapshotted once, at a fixed point in time, -// and any element still mid-load was permanently misreported as unreadable. -// raceMediaReady is the extracted wiring auditClipDurations now uses to wait -// for `loadedmetadata` up to a deadline instead. Node's built-in EventTarget -// satisfies the same duck-typed shape as a real HTMLMediaElement here, so -// this is a real test of the race/cleanup logic, not a browser mock. -describe("raceMediaReady", () => { - class FakeMediaElement extends EventTarget { - duration = NaN; - } - - it("resolves immediately when duration is already available", async () => { - const el = new FakeMediaElement(); - el.duration = 12.5; - const start = Date.now(); - await raceMediaReady(el, Date.now() + 5000); - expect(Date.now() - start).toBeLessThan(50); - }); - - it("resolves as soon as loadedmetadata fires, before the deadline", async () => { - const el = new FakeMediaElement(); - const promise = raceMediaReady(el, Date.now() + 5000); - setTimeout(() => { - el.duration = 8; - el.dispatchEvent(new Event("loadedmetadata")); - }, 20); - const start = Date.now(); - await promise; - expect(Date.now() - start).toBeLessThan(200); - }); - - it("resolves on error without hanging until the deadline", async () => { - const el = new FakeMediaElement(); - const promise = raceMediaReady(el, Date.now() + 5000); - setTimeout(() => el.dispatchEvent(new Event("error")), 20); - const start = Date.now(); - await promise; - expect(Date.now() - start).toBeLessThan(200); - }); - - it("falls back to the deadline when no event ever fires", async () => { - const el = new FakeMediaElement(); - const start = Date.now(); - await raceMediaReady(el, Date.now() + 50); - expect(Date.now() - start).toBeGreaterThanOrEqual(40); - }); -}); - -describe("shouldIgnoreRequestFailure", () => { - it("ignores aborted media preload requests", () => { - expect( - shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/sfx.wav", "net::ERR_ABORTED"), - ).toBe(true); - expect(shouldIgnoreRequestFailure("http://127.0.0.1:3000/video.mp4", "net::ERR_ABORTED")).toBe( - true, - ); - expect( - shouldIgnoreRequestFailure( - "https://www.heygenverse.com/s/50f13ccf-9002-4d80-b567-9d4c0eac30d8/raw", - "net::ERR_ABORTED", - "media", - ), - ).toBe(true); - }); - - it("keeps non-media and non-aborted failures reportable", () => { - expect( - shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/map.png", "net::ERR_ABORTED"), - ).toBe(false); - expect( - shouldIgnoreRequestFailure( - "https://www.heygenverse.com/s/50f13ccf-9002-4d80-b567-9d4c0eac30d8/raw", - "net::ERR_ABORTED", - "xhr", - ), - ).toBe(false); - expect( - shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/sfx.wav", "net::ERR_FAILED"), - ).toBe(false); - }); -}); - -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 @@ -236,39 +105,3 @@ describe("extractCompositionErrorsFromLint", () => { expect(errors.map((e) => e.text)).toEqual(["scene-a is empty", "scene-b is empty"]); }); }); - -// Regression: `validate` used a hardcoded 10s page-navigation timeout that -// ignored --timeout, so a composition loading GSAP from a CDN