import { describe, expect, it } from "vitest";
import { openComposition } from "@hyperframes/sdk";
import { patchElementInHtml } from "../../../core/src/studio-api/helpers/sourceMutation.js";
import type { PatchOperation } from "./sourcePatcher";
import { patchOpsToSdkEditOps } from "./sdkOpMapping";
const shell = (body: string) => `
${body}`;
async function applySdkDomCutover(source: string, ops: PatchOperation[]): Promise {
const session = await openComposition(source, { history: false });
session.batch(() => {
for (const op of patchOpsToSdkEditOps("hf-target", ops)) {
session.dispatch(op);
}
});
return session.serialize();
}
const cases: Array<{ name: string; source: string; ops: PatchOperation[] }> = [
{
name: "coalesces multi-property inline style including transform and custom props",
source: shell(
'OldChild
',
),
ops: [
{ type: "inline-style", property: "transform", value: "translateX(10px)" },
{ type: "inline-style", property: "transform-origin", value: "50% 50%" },
{ type: "inline-style", property: "--x", value: "12px" },
],
},
{
name: "removes one inline style from a multi-property declaration",
source: shell(
'Old
',
),
ops: [{ type: "inline-style", property: "opacity", value: null }],
},
{
name: "preserves semicolon-bearing CSS values when updating another style",
source: shell(
'Old
',
),
ops: [{ type: "inline-style", property: "color", value: "blue" }],
},
{
name: "sets direct text content",
source: shell('Old
'),
ops: [{ type: "text-content", property: "text", value: "New" }],
},
{
name: "sets text on the single child target used by the legacy path",
source: shell('Old '),
ops: [{ type: "text-content", property: "text", value: "New" }],
},
{
name: "sets text on the single child target while preserving parent text",
source: shell('Lead Old
'),
ops: [{ type: "text-content", property: "text", value: "New" }],
},
{
name: "sets data attributes through attribute ops",
source: shell('Old
'),
ops: [{ type: "attribute", property: "mode", value: "hero" }],
},
{
name: "removes data attributes through attribute ops",
source: shell('Old
'),
ops: [{ type: "attribute", property: "mode", value: null }],
},
{
name: "sets allowed html attributes",
source: shell('Old '),
ops: [{ type: "html-attribute", property: "aria-label", value: "Primary link" }],
},
{
name: "sets shorthand over existing longhands (inset over top/right/bottom/left)",
source: shell(
'Old
',
),
ops: [{ type: "inline-style", property: "inset", value: "0" }],
},
{
name: "sets longhand over existing shorthand (top over inset)",
source: shell('Old
'),
ops: [{ type: "inline-style", property: "top", value: "10px" }],
},
{
name: "mixed-type batch: inline-style + text-content in one op list",
source: shell('Old
'),
ops: [
{ type: "inline-style", property: "color", value: "blue" },
{ type: "text-content", property: "text", value: "New" },
],
},
{
name: "mixed-type batch: inline-style + attribute in one op list",
source: shell('Old
'),
ops: [
{ type: "inline-style", property: "opacity", value: "1" },
{ type: "attribute", property: "mode", value: "hero" },
],
},
];
describe("SDK cutover DOM serialization parity", () => {
it.each(cases)("$name", async ({ source, ops }) => {
const legacy = patchElementInHtml(source, { hfId: "hf-target" }, ops);
expect(legacy.matched).toBe(true);
await expect(applySdkDomCutover(source, ops)).resolves.toBe(legacy.html);
});
});