mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 12:28:12 +00:00
fix(studio): harden composition timeline reliability (#2615)
* fix(studio): preserve composition playback continuity * feat(studio): drag compositions into the timeline * fix(studio): collapse expanded composition move aliases * fix(studio): make timeline cuts atomic * fix(studio): group inspector gesture history * test(studio): cover masked text selection * fix(studio): harden composition timeline reliability * fix(studio): satisfy CI source gates * fix(studio): harden composition mutation requests
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
// @vitest-environment jsdom
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { patchElementInHtml } from "@hyperframes/studio-server/source-mutation";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDomEditStylePatchOperation } from "./domEditing";
|
||||
|
||||
const fixtureDir = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../tests/e2e/fixtures/composition-reliability",
|
||||
);
|
||||
|
||||
function fixture(path: string): string {
|
||||
return readFileSync(join(fixtureDir, path), "utf8");
|
||||
}
|
||||
|
||||
function parse(html: string): Document {
|
||||
return new DOMParser().parseFromString(html, "text/html");
|
||||
}
|
||||
|
||||
function inTemplate(document: Document, selector: string): Element | null {
|
||||
for (const template of Array.from(document.querySelectorAll("template"))) {
|
||||
const match = template.content.querySelector(selector);
|
||||
if (match) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("composition reliability acceptance fixture", () => {
|
||||
const indexSource = fixture("index.html");
|
||||
const titleSource = fixture("compositions/title-card.html");
|
||||
const nestedSource = fixture("compositions/nested-shell.html");
|
||||
|
||||
it("owns repeated root hosts, a nested host, transparent headline topology, and collisions", () => {
|
||||
const index = parse(indexSource);
|
||||
const repeated = Array.from(
|
||||
index.querySelectorAll('[data-composition-src="compositions/title-card.html"]'),
|
||||
);
|
||||
expect(repeated).toHaveLength(2);
|
||||
expect(repeated.map((host) => host.getAttribute("data-start"))).toEqual(["0", "4"]);
|
||||
expect(
|
||||
index.querySelector('[data-composition-src="compositions/nested-shell.html"]'),
|
||||
).toBeTruthy();
|
||||
|
||||
const nested = parse(nestedSource);
|
||||
const nestedHost = inTemplate(nested, '[data-composition-src="title-card.html"]');
|
||||
expect(nestedHost).toBeTruthy();
|
||||
const nestedDependency = nestedHost?.getAttribute("data-composition-src");
|
||||
expect(
|
||||
nestedDependency ? existsSync(resolve(fixtureDir, "compositions", nestedDependency)) : false,
|
||||
).toBe(true);
|
||||
|
||||
const title = parse(titleSource);
|
||||
const mask = inTemplate(title, ".hl-mask");
|
||||
const headline = inTemplate(title, ".hl-mask > .hl-text");
|
||||
expect(mask?.textContent).toContain("Reliable compositions");
|
||||
expect(inTemplate(title, "style")?.textContent).toMatch(
|
||||
/\.hl-mask\s*\{[^}]*overflow:\s*hidden;[^}]*background:\s*transparent;/,
|
||||
);
|
||||
expect(headline?.tagName).toBe("H1");
|
||||
|
||||
const collisionA = index.querySelector('[data-hf-id="collision-a"]')!;
|
||||
const collisionB = index.querySelector('[data-hf-id="collision-b"]')!;
|
||||
const layered = index.querySelector('[data-hf-id="layer-overlap"]')!;
|
||||
expect(collisionA.getAttribute("data-track-index")).toBe(
|
||||
collisionB.getAttribute("data-track-index"),
|
||||
);
|
||||
expect(
|
||||
Number(collisionA.getAttribute("data-start")) +
|
||||
Number(collisionA.getAttribute("data-duration")),
|
||||
).toBe(Number(collisionB.getAttribute("data-start")));
|
||||
expect(layered.getAttribute("data-start")).toBe(collisionB.getAttribute("data-start"));
|
||||
expect(layered.getAttribute("data-track-index")).not.toBe(
|
||||
collisionB.getAttribute("data-track-index"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps timeline host edits in the root source and headline color in the template source", () => {
|
||||
const moved = patchElementInHtml(indexSource, { hfId: "title-host-a" }, [
|
||||
{ type: "attribute", property: "start", value: "5" },
|
||||
{ type: "attribute", property: "duration", value: "2" },
|
||||
]);
|
||||
expect(moved.matched).toBe(true);
|
||||
expect(moved.html).toContain('data-hf-id="title-host-a"');
|
||||
expect(moved.html).toContain('data-start="5"');
|
||||
expect(moved.html).toContain('data-duration="2"');
|
||||
expect(moved.html).not.toContain('data-hf-id="title-text"');
|
||||
expect(titleSource).not.toContain("#12b886");
|
||||
|
||||
const recolored = patchElementInHtml(titleSource, { hfId: "title-text" }, [
|
||||
buildDomEditStylePatchOperation("color", "#12b886"),
|
||||
]);
|
||||
expect(recolored.matched).toBe(true);
|
||||
const recoloredDocument = parse(recolored.html);
|
||||
expect(
|
||||
inTemplate(recoloredDocument, '[data-hf-id="title-text"]')?.getAttribute("style"),
|
||||
).toContain("color: #12b886");
|
||||
expect(
|
||||
inTemplate(recoloredDocument, '[data-hf-id="title-mask"]')?.getAttribute("style"),
|
||||
).toBeNull();
|
||||
expect(recolored.html).not.toContain('data-hf-id="title-host-a"');
|
||||
expect(indexSource).not.toContain("#12b886");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user