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 = `
`;
const op: PatchOperation = { type: "inline-style", property: "z-index", value: "3" };
const result = applyPatch(html, "gif-img", op);
expect(result).toBe(
`
`,
);
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 =
'Outside
';
const patched = applyPatchByTarget(
html,
{ id: "card" },
{
type: "text-content",
property: "text",
value: "New headline",
},
);
expect(patched).toBe(
'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
');
});
});