Files
hyperframes/packages/cli/src/commands/compositions.test.ts
T
Miguel Ángel 0b3dfb3f84 fix(render): consolidate duration and timing correctness (#2405)
* fix(producer): pass variables to duration probe

* fix(producer): tolerate rounded frame-boundary durations

* fix(cli): resolve relative data-start references in composition duration

`compositions --json` computed each timed child's start with a bare
parseFloat(data-start ?? "0") in parseCompositions (host duration) and
parseSubComposition (sub-comp duration). A relative reference like
data-start="s1" ("start when clip s1 ends") is not numeric, so parseFloat
returned NaN and that clip's contribution to the max-end was silently
dropped — a host with two 3s clips (2nd data-start="s1") reported duration 3
instead of 6, breaking compositions/inspect/snapshot for composition-clip
relative timing.

Resolve relative references the same way the extractor does (parseStartExpression
from @hyperframes/core + a findReferenceTargetEl/resolveReferencedStart port,
since the engine's referenceResolver isn't a public export across the package
boundary). Verified: host duration now 6; 3 tests pass.
(Implemented via Codex; verified independently.)
2026-07-14 17:12:36 -04:00

90 lines
2.9 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, beforeEach } from "vitest";
import { ensureDOMParser } from "../utils/dom.js";
import { parseCompositions, parseSubComposition } from "./compositions.js";
describe("parseCompositions", () => {
beforeEach(() => {
ensureDOMParser();
});
it("resolves relative sub-composition starts when computing host duration", () => {
const baseDir = mkdtempSync(join(tmpdir(), "hyperframes-compositions-"));
try {
const compositionsDir = join(baseDir, "compositions");
mkdirSync(compositionsDir);
const subCompositionHtml = `
<template id="scene-template">
<div data-composition-id="scene" data-width="1920" data-height="1080" data-duration="3">
<div class="clip" data-start="0" data-duration="3"></div>
</div>
</template>`;
writeFileSync(join(compositionsDir, "scene.html"), subCompositionHtml);
const html = `
<div data-composition-id="host" data-width="1920" data-height="1080">
<div id="s1" data-composition-id="s1" data-composition-src="compositions/scene.html" data-start="0" data-duration="3" data-track="main"></div>
<div data-composition-id="s2" data-composition-src="compositions/scene.html" data-start="s1" data-duration="3" data-track="main"></div>
</div>`;
const host = parseCompositions(html, baseDir).find(
(composition) => composition.id === "host",
);
expect(host?.duration).toBe(6);
} finally {
rmSync(baseDir, { recursive: true, force: true });
}
});
});
describe("parseSubComposition", () => {
beforeEach(() => {
ensureDOMParser();
});
it("reads template-wrapped sub-composition contents", () => {
const html = `
<template id="foo-template">
<div data-composition-id="foo" data-width="1920" data-height="1080" data-duration="1.5">
<div class="clip" data-start="0" data-duration="1.5"></div>
</div>
</template>`;
expect(parseSubComposition(html, "foo", 1280, 720)).toEqual({
id: "foo",
duration: 1.5,
width: 1920,
height: 1080,
elementCount: 1,
});
});
it("counts visual-only template content and estimates simple script durations", () => {
const html = `
<template id="foo-template">
<div data-composition-id="foo" data-width="1920" data-height="1080">
<div class="foo-bg" style="position:absolute;inset:0;background:#f00;"></div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(".foo-bg", { opacity: 0 }, { opacity: 1, duration: 0.5 }, 0);
window.__timelines["foo"] = tl;
</script>
</div>
</template>`;
expect(parseSubComposition(html, "foo", 1280, 720)).toEqual({
id: "foo",
duration: 0.5,
width: 1920,
height: 1080,
elementCount: 1,
});
});
});