From 2e8f871bc86d29ec3369f0eb11f0183b2001a07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 16 Jul 2026 00:20:57 +0000 Subject: [PATCH] fix(video): hold final frame through composition --- packages/cli/src/commands/validate.test.ts | 53 +++++++++++ packages/cli/src/commands/validate.ts | 7 +- .../core/src/compiler/htmlCompiler.test.ts | 30 ++++++- packages/core/src/compiler/htmlCompiler.ts | 16 ++-- packages/core/src/compiler/index.ts | 1 + .../core/src/compiler/timingCompiler.test.ts | 8 ++ packages/core/src/compiler/timingCompiler.ts | 20 ++++- packages/core/src/index.test.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/runtime/init.ts | 16 ++-- packages/core/src/runtime/media.test.ts | 88 ++++++++++++++++++- packages/core/src/runtime/media.ts | 58 ++++++++++-- .../src/services/videoFrameExtractor.test.ts | 42 +++++++-- .../src/services/videoFrameExtractor.ts | 83 ++++++++--------- .../producer/src/services/htmlCompiler.ts | 18 ++-- 15 files changed, 352 insertions(+), 90 deletions(-) diff --git a/packages/cli/src/commands/validate.test.ts b/packages/cli/src/commands/validate.test.ts index 3fe07fbfb..f706181ef 100644 --- a/packages/cli/src/commands/validate.test.ts +++ b/packages/cli/src/commands/validate.test.ts @@ -11,6 +11,7 @@ import { runAndParseJsonEnvelope, } from "./deprecationTestHarness.js"; import { + auditClipDurations, extractCompositionErrorsFromLint, navigationTimeoutHint, raceMediaReady, @@ -49,6 +50,58 @@ vi.mock("../utils/producer.js", () => ({ vi.mock("../utils/project.js", () => resolveProjectMock()); vi.mock("../utils/lintProject.js", () => lintProjectFailureMock()); +describe("auditClipDurations", () => { + it("audits audio only because explicit video slots hold their final frame", async () => { + let selector = ""; + const originalDocument = globalThis.document; + const audio = { + duration: 1, + id: "voice", + loop: false, + tagName: "AUDIO", + getAttribute: (name: string) => + name === "data-duration" ? "5" : name === "data-media-start" ? "0" : null, + }; + const page = { + evaluate: async (fn: (waitMs: number) => unknown, waitMs: number) => { + Object.defineProperty(globalThis, "document", { + configurable: true, + value: { + querySelectorAll: (query: string) => { + selector = query; + return [audio]; + }, + }, + }); + return fn(waitMs); + }, + }; + + try { + const warnings = await auditClipDurations( + page as never, + ({ slotSeconds, mediaSeconds }) => ({ + shortfallSeconds: slotSeconds - mediaSeconds, + toleranceSeconds: 0.05, + }), + 10, + ); + expect(selector).toBe("audio[data-duration]"); + expect(warnings).toHaveLength(1); + expect(warnings[0]?.text).toContain('Audio "voice"'); + } finally { + if (originalDocument === undefined) { + Reflect.deleteProperty(globalThis, "document"); + } else { + Object.defineProperty(globalThis, "document", { + configurable: true, + value: originalDocument, + }); + } + } + }); +}); + // 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. diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 2610aa725..3973e422c 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -127,8 +127,9 @@ export function raceMediaReady( } /** - * Flag `