// @vitest-environment jsdom import { describe, expect, it } from "vitest"; import { createTimelineElementFromManifestClip, parseTimelineFromDOM, createImplicitTimelineLayersFromDOM, mergeTimelineElementsPreservingDowngrades, } from "./timelineDOM"; import type { TimelineElement } from "../store/playerStore"; function el(id: string, extra: Partial = {}): TimelineElement { return { id, tag: "img", start: 0, duration: 5, track: 0, ...extra }; } function makeDoc(html: string): Document { const d = document.implementation.createHTMLDocument(); d.body.innerHTML = html; return d; } describe("parseTimelineFromDOM — hfId from data-hf-id", () => { it("harvests hfId from a data-start element that has data-hf-id", () => { const doc = makeDoc(`
`); const elements = parseTimelineFromDOM(doc, 10); const hero = elements.find((el) => el.domId === "hero"); expect(hero).toBeDefined(); expect(hero?.hfId).toBe("hf-abc123"); }); it("leaves hfId undefined when element has no data-hf-id", () => { const doc = makeDoc(`
`); const elements = parseTimelineFromDOM(doc, 10); const plain = elements.find((el) => el.domId === "plain"); expect(plain).toBeDefined(); expect(plain?.hfId).toBeUndefined(); }); it("ignores runtime-owned color grading canvases with timing attributes", () => { const doc = makeDoc(`
`); const elements = parseTimelineFromDOM(doc, 10); expect(elements.map((el) => el.tag)).toEqual(["img"]); }); it("marks parsed timeline elements hidden when data-hidden is present", () => { const doc = makeDoc(`
`); const elements = parseTimelineFromDOM(doc, 10); const hero = elements.find((el) => el.domId === "hero"); expect(hero?.hidden).toBe(true); }); it("marks manifest timeline elements hidden when the host has data-hidden", () => { const doc = makeDoc(`
`); const hostEl = doc.getElementById("hero"); const element = createTimelineElementFromManifestClip({ clip: { id: "hero", label: "Hero", kind: "element", tagName: "div", start: 0, duration: 5, track: 0, compositionId: null, parentCompositionId: null, compositionSrc: null, assetUrl: null, }, fallbackIndex: 0, doc, hostEl, }); expect(element.hidden).toBe(true); }); }); describe("createTimelineElementFromManifestClip — source-scoped selector identity", () => { it("ignores an index.html duplicate when indexing a scene.html selector", () => { const doc = makeDoc(`
`); const target = doc.querySelector("[data-target]"); if (!target) throw new Error("missing target"); const element = createTimelineElementFromManifestClip({ clip: { id: null, label: "Sub", kind: "element", tagName: "div", start: 0, duration: 5, track: 0, compositionId: null, parentCompositionId: null, compositionSrc: null, assetUrl: null, }, fallbackIndex: 0, doc, hostEl: target, }); expect(element.sourceFile).toBe("scene.html"); expect(element.selectorIndex).toBe(1); expect(element.key).toBe("scene.html:.sub:1"); }); }); describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => { it("uses the runtime root paint scope for implicit siblings of manifest clips", () => { const doc = makeDoc(`
`); const timedHost = doc.getElementById("timed"); const timed = createTimelineElementFromManifestClip({ clip: { id: "timed", label: "Timed", start: 0, duration: 5, track: 0, stackingContextId: "css:root", kind: "element", tagName: "div", compositionId: null, parentCompositionId: null, compositionSrc: null, assetUrl: null, }, fallbackIndex: 0, doc, hostEl: timedHost, }); const implicit = createImplicitTimelineLayersFromDOM(doc, 5, [timed])[0]; expect(implicit?.stackingContextId).toBe("css:root"); expect(implicit?.stackingContextId).toBe(timed.stackingContextId); }); it("harvests hfId from an implicit layer child that has data-hf-id", () => { const doc = makeDoc(`
`); const layers = createImplicitTimelineLayersFromDOM(doc, 10); const layer = layers.find((el) => el.domId === "layer"); expect(layer).toBeDefined(); expect(layer?.hfId).toBe("hf-xyz789"); }); it("ignores runtime-owned color grading canvases as implicit layers", () => { const doc = makeDoc(`
`); const layers = createImplicitTimelineLayersFromDOM(doc, 5); expect(layers).toEqual([]); }); }); describe("mergeTimelineElementsPreservingDowngrades — genuine removal vs transient downgrade", () => { it("drops a removed TOP-LEVEL element (undo of a split) instead of ghosting it", () => { const current = [el("a"), el("a-split")]; // post-split store: original + clone const next = [el("a")]; // fresh scan of the reverted file: clone gone const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30); expect(merged.map((e) => e.id)).toEqual(["a"]); }); it("still preserves an enriched sub-composition child a bare re-scan drops", () => { const current = [el("a"), el("sub-child", { compositionSrc: "sub.html" })]; const next = [el("a")]; // bare DOM scan misses the enriched sub-comp child const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30); expect(merged.map((e) => e.id).sort()).toEqual(["a", "sub-child"]); }); it("trusts the fresh scan fully when it is not shorter", () => { const current = [el("a"), el("b", { compositionSrc: "sub.html" })]; const next = [el("a"), el("c")]; expect( mergeTimelineElementsPreservingDowngrades(current, next, 30, 30).map((e) => e.id), ).toEqual(["a", "c"]); }); });