import { describe, expect, it } from "vitest"; import { applyPatch, applyPatchByTarget, readAttributeByTarget, readTagSnippetByTarget, type PatchOperation, } from "./sourcePatcher"; describe("applyPatchByTarget", () => { it("updates a composition host by data-composition-id selector", () => { const html = `
`; const op: PatchOperation = { type: "attribute", property: "start", value: "2.5" }; expect(applyPatchByTarget(html, { selector: '[data-composition-id="intro"]' }, op)).toContain( 'data-start="2.5"', ); }); it("updates a class-based layer when the clip has no DOM id", () => { const html = `
`; const op: PatchOperation = { type: "attribute", property: "track-index", value: "3" }; expect(applyPatchByTarget(html, { selector: ".headline" }, op)).toContain( 'data-track-index="3"', ); }); it("updates inline z-index by selector when the clip has no DOM id", () => { const html = `
`; const op: PatchOperation = { type: "inline-style", property: "z-index", value: "3" }; expect(applyPatchByTarget(html, { selector: ".headline" }, op)).toContain( 'style="position: absolute; opacity: 1; z-index: 3"', ); }); it("adds inline style to a self-closing void element without malforming it", () => { const html = `earth`; const op: PatchOperation = { type: "inline-style", property: "z-index", value: "3" }; const result = applyPatch(html, "gif-img", op); expect(result).toBe( `earth`, ); expect(result).not.toContain("/ style"); }); it("adds inline style to a self-closing void element matched by selector", () => { const html = ``; const op: PatchOperation = { type: "inline-style", property: "opacity", value: "0.5" }; const result = applyPatchByTarget(html, { selector: ".hero" }, op); expect(result).toBe( ``, ); expect(result).not.toContain("/ style"); }); it("patches inline move styles by target", () => { const html = `
`; const withLeft = applyPatchByTarget( html, { id: "card" }, { type: "inline-style", property: "left", value: "160px" }, ); const withTop = applyPatchByTarget( withLeft, { id: "card" }, { type: "inline-style", property: "top", value: "140px" }, ); expect(withTop).toContain('style="position: absolute; left: 160px; top: 140px"'); }); it("patches inline resize styles by target", () => { const html = `
`; const withWidth = applyPatchByTarget( html, { id: "card" }, { type: "inline-style", property: "width", value: "420px" }, ); const withHeight = applyPatchByTarget( withWidth, { id: "card" }, { type: "inline-style", property: "height", value: "220px" }, ); expect(withHeight).toContain('style="position: absolute; width: 420px; height: 220px"'); }); it("escapes quoted CSS urls inside double-quoted style attributes", () => { const html = `
`; const withBackground = applyPatchByTarget( html, { id: "card" }, { type: "inline-style", property: "background-image", value: `url("../ChatGPT Image Apr 22, 2026.png")`, }, ); const withRadius = applyPatchByTarget( withBackground, { id: "card" }, { type: "inline-style", property: "border-radius", value: "12px" }, ); expect(withRadius).toContain( "background-image: url("../ChatGPT Image Apr 22, 2026.png")", ); expect(withRadius).toContain("border-radius: 12px"); }); it("updates media timing attributes by selector", () => { const html = ``; const withDuration = applyPatchByTarget( html, { selector: ".hero" }, { type: "attribute", property: "duration", value: "1.1", }, ); const withMediaStart = applyPatchByTarget( withDuration, { selector: ".hero" }, { type: "attribute", property: "media-start", value: "0.7", }, ); expect(withMediaStart).toContain('data-duration="1.1"'); expect(withMediaStart).toContain('data-media-start="0.7"'); }); it("reads media timing attributes by selector", () => { const html = `
`; expect(readAttributeByTarget(html, { selector: ".hero" }, "media-start")).toBe("0.4"); expect(readAttributeByTarget(html, { selector: ".hero" }, "duration")).toBe("1.4"); }); it("reads the matched tag snippet by target", () => { const html = `
`; expect(readTagSnippetByTarget(html, { id: "hero" })).toBe( `
{ const html = "
"; const moved = applyPatchByTarget( html, { id: "hero" }, { type: "inline-style", property: "left", value: "160px" }, ); const updated = applyPatchByTarget( moved, { id: "hero" }, { type: "attribute", property: "start", value: "0.4" }, ); expect(updated).toContain(`style='left: 160px; top: 180px'`); expect(updated).toContain(`data-start="0.4"`); expect(readAttributeByTarget(updated, { id: "hero" }, "start")).toBe("0.4"); }); it("replaces the full text body of a nested element by id", () => { const html = '
HeadlineSupporting copy

Outside

'; const patched = applyPatch(html, "panel", { type: "text-content", property: "text", value: "New headlineNew supporting copy", }); expect(patched).toContain( '
New headlineNew supporting copy
', ); expect(patched).toContain("

Outside

"); }); it("does not stop at the first child closing tag when patching nested text", () => { const html = '
Headline
Copy

Outside

'; const patched = applyPatchByTarget( html, { id: "card" }, { type: "text-content", property: "text", value: "New headline", }, ); expect(patched).toBe( '
New headline

Outside

', ); }); it("patches the correct duplicate selector occurrence", () => { const html = [ `
`, `
`, ].join(""); const patched = applyPatchByTarget( html, { selector: ".headline", selectorIndex: 1 }, { type: "attribute", property: "start", value: "2.5", }, ); expect(patched).toContain(`
`); expect(patched).toContain(`
`); }); it("escapes JSON attribute values containing double-quotes and round-trips them", () => { const html = `
`; const motionJson = JSON.stringify({ preset: "fadeIn", start: 0, duration: 1.5 }); const patched = applyPatch(html, "card", { type: "attribute", property: "data-hf-studio-motion", value: motionJson, }); // The raw HTML must NOT contain unescaped quotes inside the attribute expect(patched).not.toMatch(/data-hf-studio-motion="[^"]*"[^"]*"/); // Entities should be present expect(patched).toContain("""); // Reading the attribute back should return the original JSON const readBack = readAttributeByTarget(patched, { id: "card" }, "data-hf-studio-motion"); expect(readBack).toBe(motionJson); }); it("escapes and round-trips data-hf-studio-motion-original-transform with quotes", () => { const html = `
`; const transform = `rotate(15deg) translate("50px", "100px")`; const patched = applyPatchByTarget( html, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-transform", value: transform, }, ); // No broken attribute boundary expect(patched).not.toMatch(/data-hf-studio-motion-original-transform="[^"]*"[^"]*"/); const readBack = readAttributeByTarget( patched, { id: "hero" }, "data-hf-studio-motion-original-transform", ); expect(readBack).toBe(transform); }); it("escapes ampersands and angle brackets in attribute values", () => { const html = `
`; const value = `a&bd"e`; const patched = applyPatch(html, "el", { type: "attribute", property: "data-custom", value, }); expect(patched).toContain("a&b<c>d"e"); const readBack = readAttributeByTarget(patched, { id: "el" }, "data-custom"); expect(readBack).toBe(value); }); it("updates an already-escaped attribute value to a new escaped value", () => { const html = `
`; const first = JSON.stringify({ preset: "fadeIn" }); const second = JSON.stringify({ preset: "slideUp", easing: "ease-out" }); const patched1 = applyPatch(html, "card", { type: "attribute", property: "data-hf-studio-motion", value: first, }); const patched2 = applyPatch(patched1, "card", { type: "attribute", property: "data-hf-studio-motion", value: second, }); const readBack = readAttributeByTarget(patched2, { id: "card" }, "data-hf-studio-motion"); expect(readBack).toBe(second); }); }); describe("motion attribute round-trip via sourcePatcher", () => { it("round-trips data-hf-studio-motion JSON through patch and read", () => { const html = `
Hero
`; const motion = { start: 0.5, duration: 1, ease: "power3.out", from: { opacity: 0, y: 40 }, to: { opacity: 1, y: 0 }, }; const motionJson = JSON.stringify(motion); const patched = applyPatchByTarget( html, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion", value: motionJson }, ); const readBack = readAttributeByTarget(patched, { id: "hero" }, "data-hf-studio-motion"); expect(readBack).toBeDefined(); expect(JSON.parse(readBack!)).toEqual(motion); }); it("round-trips motion with customEase containing SVG path data", () => { const html = `
Card
`; const motion = { start: 0.25, duration: 0.8, ease: "studio-card-bounce", customEase: { id: "studio-card-bounce", data: "M0,0 C0.18,0.9 0.32,1 1,1" }, from: { y: 44, autoAlpha: 0 }, to: { y: 0, autoAlpha: 1 }, }; const motionJson = JSON.stringify(motion); const patched = applyPatchByTarget( html, { id: "card" }, { type: "attribute", property: "data-hf-studio-motion", value: motionJson }, ); const readBack = readAttributeByTarget(patched, { id: "card" }, "data-hf-studio-motion"); expect(readBack).toBeDefined(); expect(JSON.parse(readBack!)).toEqual(motion); }); it("round-trips all four motion attributes (motion + three originals)", () => { const html = `
Hero
`; const motion = { start: 0, duration: 0.6, ease: "power2.out", from: { autoAlpha: 0, y: 32 }, to: { autoAlpha: 1, y: 0 }, }; let result = html; result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion", value: JSON.stringify(motion) }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-transform", value: "rotate(5deg)", }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-opacity", value: "0.8" }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-visibility", value: "visible", }, ); expect( JSON.parse(readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion")!), ).toEqual(motion); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-transform"), ).toBe("rotate(5deg)"); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-opacity"), ).toBe("0.8"); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-visibility"), ).toBe("visible"); }); it("removes all four motion attributes when clearing", () => { const html = `
Hero
`; const motion = { start: 0, duration: 1, ease: "none", from: { opacity: 0 }, to: { opacity: 1 }, }; let result = html; result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion", value: JSON.stringify(motion) }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-transform", value: "" }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-opacity", value: "1" }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-visibility", value: "" }, ); // Verify all four attributes exist expect(readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion")).toBeDefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-transform"), ).toBeDefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-opacity"), ).toBeDefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-visibility"), ).toBeDefined(); // Remove all four result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion", value: null }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-transform", value: null }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-opacity", value: null }, ); result = applyPatchByTarget( result, { id: "hero" }, { type: "attribute", property: "data-hf-studio-motion-original-visibility", value: null }, ); expect(readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion")).toBeUndefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-transform"), ).toBeUndefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-opacity"), ).toBeUndefined(); expect( readAttributeByTarget(result, { id: "hero" }, "data-hf-studio-motion-original-visibility"), ).toBeUndefined(); }); it("round-trips motion via selector when element has no id", () => { const html = `
Title
`; const motion = { start: 0.3, duration: 0.5, ease: "sine.out", from: { scale: 0.88, autoAlpha: 0 }, to: { scale: 1, autoAlpha: 1 }, }; const patched = applyPatchByTarget( html, { selector: ".headline" }, { type: "attribute", property: "data-hf-studio-motion", value: JSON.stringify(motion) }, ); const readBack = readAttributeByTarget( patched, { selector: ".headline" }, "data-hf-studio-motion", ); expect(readBack).toBeDefined(); expect(JSON.parse(readBack!)).toEqual(motion); }); }); // T3 — id-based targeting (R1). describe("T3 — hfId targeting (spec for R1)", () => { it("updates inline style by data-hf-id", () => { const html = `

Hello

`; const result = applyPatchByTarget( html, { hfId: "hf-x7k2" }, { type: "inline-style", property: "color", value: "blue", }, ); expect(result).toContain("color: blue"); expect(result).toContain('data-hf-id="hf-x7k2"'); }); it("updates text content by data-hf-id", () => { const html = `

Old text

`; const result = applyPatchByTarget( html, { hfId: "hf-a1b2" }, { type: "text-content", property: "", value: "New text", }, ); expect(result).toContain(">New text<"); }); it("updates attribute by data-hf-id", () => { const html = `
`; const result = applyPatchByTarget( html, { hfId: "hf-c3d4" }, { type: "attribute", property: "start", value: "2.5", }, ); expect(result).toContain('data-start="2.5"'); }); it("data-hf-id attribute is preserved after a style patch", () => { const html = `

Hello

`; const patched = applyPatchByTarget( html, { hfId: "hf-x7k2" }, { type: "inline-style", property: "color", value: "blue", }, ); expect(readAttributeByTarget(patched, { hfId: "hf-x7k2" }, "data-hf-id")).toBe("hf-x7k2"); }); it("hfId lookup falls through to selector when hfId not found", () => { const html = `

Hello

`; const result = applyPatchByTarget( html, { hfId: "hf-missing", selector: ".headline" }, { type: "inline-style", property: "color", value: "blue" }, ); expect(result).toContain("color: blue"); }); it("hfId match is authoritative — selector is not used as a narrowing filter", () => { // hfId matches h1; selector points at h2. hfId wins — patch lands on h1, h2 untouched. const html = `

A

B

`; const result = applyPatchByTarget( html, { hfId: "hf-x7k2", selector: ".b" }, { type: "inline-style", property: "color", value: "blue" }, ); expect(result).toContain('data-hf-id="hf-x7k2"'); const h1End = result.indexOf(""); const bluePos = result.indexOf("color: blue"); expect(bluePos).toBeGreaterThan(-1); expect(bluePos).toBeLessThan(h1End); expect(result).toContain('

B

'); }); });