mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
/**
|
|
* Template-based sub-comp support — SDK must model elements inside <template>.
|
|
*
|
|
* The studio preview unwraps <template data-composition-id> content into the
|
|
* served body, so the timeline hands edits hf-ids that live inside the
|
|
* template in the raw file. Before this support, buildElement excluded the
|
|
* whole template subtree: getElements() returned [] for template comps and
|
|
* every edit produced a false element_not_found resolver divergence.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { openComposition } from "./session.js";
|
|
|
|
const TEMPLATE_HTML = `
|
|
<template data-composition-id="test-minimal">
|
|
<div data-hf-id="hf-a" class="clip" data-start="0" data-end="3">Hello</div>
|
|
<div data-hf-id="hf-b" class="clip" data-start="3" data-end="6">World</div>
|
|
</template>
|
|
`.trim();
|
|
|
|
const TEMPLATE_UNSTAMPED_HTML = `
|
|
<template data-composition-id="test-minimal">
|
|
<div class="clip" data-start="0" data-end="3">Hello</div>
|
|
</template>
|
|
`.trim();
|
|
|
|
describe("template-based sub-comp compositions", () => {
|
|
it("getElements() models the template's inner elements (template itself absent)", async () => {
|
|
const comp = await openComposition(TEMPLATE_HTML);
|
|
const els = comp.getElements();
|
|
expect(els.map((e) => e.id)).toEqual(["hf-a", "hf-b"]);
|
|
expect(els.every((e) => e.tag !== "template")).toBe(true);
|
|
});
|
|
|
|
it("mints ids for unstamped template-inner elements on open", async () => {
|
|
const comp = await openComposition(TEMPLATE_UNSTAMPED_HTML);
|
|
expect(comp.getElements()).toHaveLength(1);
|
|
expect(comp.getElements()[0]?.id).toMatch(/^hf-/);
|
|
});
|
|
|
|
it("getElement resolves a template-inner id", async () => {
|
|
const comp = await openComposition(TEMPLATE_HTML);
|
|
expect(comp.getElement("hf-a")?.text).toBe("Hello");
|
|
});
|
|
|
|
it("setTiming on a template-inner element mutates and serializes inside the template", async () => {
|
|
const comp = await openComposition(TEMPLATE_HTML);
|
|
comp.setTiming("hf-a", { start: 1.5 });
|
|
const out = comp.serialize();
|
|
expect(out).toContain("<template");
|
|
// the mutated start must be INSIDE the template wrapper
|
|
const tpl = out.slice(out.indexOf("<template"), out.indexOf("</template>"));
|
|
expect(tpl).toContain('data-start="1.5"');
|
|
expect(comp.getElement("hf-a")?.start).toBe(1.5);
|
|
});
|
|
|
|
it("timed template-inner elements carry start/duration snapshots", async () => {
|
|
const comp = await openComposition(TEMPLATE_HTML);
|
|
const a = comp.getElement("hf-a");
|
|
expect(a?.start).toBe(0);
|
|
expect(a?.duration).toBe(3);
|
|
});
|
|
|
|
it("plain <template> (runtime clone-source) stays fully excluded", async () => {
|
|
const comp = await openComposition(
|
|
`<div data-hf-id="hf-stage" data-hf-root>x</div><template><li data-hf-id="hf-clone">item</li></template>`,
|
|
);
|
|
expect(comp.getElements().map((e) => e.id)).toEqual(["hf-stage"]);
|
|
expect(comp.getElement("hf-clone")).toBeNull();
|
|
});
|
|
|
|
it("duplicate hf-id resolves in true document order (template-inner first when it comes first)", async () => {
|
|
// A comp-template-inner element EARLIER in the document and a top-level
|
|
// element LATER share an id (copy-paste drift). The preview's unwrapped
|
|
// DOM resolves the first-in-document copy; the SDK must agree.
|
|
const comp = await openComposition(
|
|
`<template data-composition-id="t"><div data-hf-id="hf-dup" data-start="1" data-end="2">tpl</div></template><div data-hf-id="hf-dup" data-start="5" data-end="6">top</div>`,
|
|
);
|
|
expect(comp.getElement("hf-dup")?.text).toBe("tpl");
|
|
});
|
|
|
|
it("models GSAP animations declared inside a composition template", async () => {
|
|
const comp = await openComposition(`
|
|
<template data-composition-id="document-card">
|
|
<div data-hf-id="hf-line" class="line">line</div>
|
|
<script>
|
|
var tl = gsap.timeline({ paused: true });
|
|
tl.to("[data-hf-id=\\"hf-line\\"]", { x: 100, duration: 1 }, 0);
|
|
</script>
|
|
</template>
|
|
`);
|
|
|
|
const animationIds = comp.getElement("hf-line")?.animationIds ?? [];
|
|
expect(animationIds).toHaveLength(1);
|
|
expect(comp.getAllAnimationIds()).toEqual(new Set(animationIds));
|
|
});
|
|
});
|
|
|
|
// The authored sub-comp form `hyperframes add` scaffolds: the composition id is
|
|
// on the wrapped root div, and the <template> is keyed by `id="X-template"`.
|
|
const AUTHORED_TEMPLATE_HTML = `
|
|
<template id="card-template">
|
|
<div data-composition-id="card" data-width="1280" data-height="720" data-duration="5">
|
|
<h1 class="title" style="color: rgb(255, 0, 0)">Headline</h1>
|
|
</div>
|
|
</template>
|
|
`.trim();
|
|
|
|
describe("authored template sub-comps (id on the wrapped root div)", () => {
|
|
it("enumerates and resolves inner elements", async () => {
|
|
const comp = await openComposition(AUTHORED_TEMPLATE_HTML);
|
|
const title = comp.getElements().find((e) => e.classNames.includes("title"));
|
|
expect(title).toBeTruthy();
|
|
expect(comp.getElement(title!.id)?.text).toBe("Headline");
|
|
});
|
|
|
|
it("declares a variable on the root div and round-trips through serialize", async () => {
|
|
const comp = await openComposition(AUTHORED_TEMPLATE_HTML);
|
|
comp.declareVariable({
|
|
id: "title-color",
|
|
type: "color",
|
|
label: "Title color",
|
|
default: "#ff0000",
|
|
});
|
|
expect(comp.getVariableDeclarations().map((d) => d.id)).toEqual(["title-color"]);
|
|
const serialized = comp.serialize();
|
|
expect(serialized).toContain("data-composition-variables");
|
|
// Survives a re-open (declaration is on the root div, not a stripped <html>).
|
|
const reopened = await openComposition(serialized);
|
|
expect(reopened.getVariableDeclarations().map((d) => d.id)).toEqual(["title-color"]);
|
|
});
|
|
|
|
it("does not treat a plain clone-source template as a composition", async () => {
|
|
const comp = await openComposition(
|
|
`<div data-composition-id="c" data-width="100" data-height="100" data-duration="1" data-start="0">` +
|
|
`<template id="particle"><span class="dot">·</span></template></div>`,
|
|
);
|
|
// The particle template's inner <span> must NOT be enumerated (it is cloned
|
|
// N times at runtime; a persisted inner id would duplicate across clones).
|
|
expect(comp.getElements().some((e) => e.classNames.includes("dot"))).toBe(false);
|
|
});
|
|
});
|