fix(sdk,studio): restore DOM edit cutover parity (#1565)

- Add splitStyleDeclarations with quote/paren-aware CSS parsing
- Fix backslash escape handling inside quoted CSS string values
- Close html-attribute safety gap in SDK cutover (event handlers, dangerous URIs)
- Consolidate HTML attribute safety constants to core/utils/htmlAttrSafety.ts
- Extract NON_HTML_CHILD_TAGS set for foreign-content decline gate
- Add sdkCutoverParity test corpus (shorthand/longhand, mixed batches)
This commit is contained in:
Miguel Ángel
2026-06-19 15:37:10 -04:00
committed by GitHub
parent 4e32c5e0fe
commit 758eda995c
12 changed files with 534 additions and 119 deletions
@@ -98,6 +98,36 @@ describe("shouldUseSdkCutover", () => {
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("DATA-START", "1")])).toBe(false);
});
it("declines html-attribute ops with event handler names", () => {
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("onclick", "alert(1)")])).toBe(
false,
);
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("onload", "fetch()")])).toBe(
false,
);
});
it("declines html-attribute ops with disallowed attribute names", () => {
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("formaction", "/x")])).toBe(false);
});
it("declines html-attribute ops with dangerous URI schemes", () => {
expect(
shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("href", "javascript:alert(1)")]),
).toBe(false);
expect(shouldUseSdkCutover(true, true, "hf-abc", [htmlAttrOp("src", "vbscript:run")])).toBe(
false,
);
});
it("declines html-attribute ops with dangerous data URIs", () => {
expect(
shouldUseSdkCutover(true, true, "hf-abc", [
htmlAttrOp("href", "data:text/html,<script>alert(1)</script>"),
]),
).toBe(false);
});
it("returns true when ops mix all supported types", () => {
expect(
shouldUseSdkCutover(true, true, "hf-abc", [
@@ -205,6 +235,27 @@ describe("sdkCutoverPersist", () => {
});
});
it.each([
{ name: "multi-child targets", children: [{ id: "a" }, { id: "b" }] },
{ name: "single non-html children", children: [{ id: "a", tag: "svg" }] },
])("declines text-content cutover for $name", async ({ children }) => {
const deps = makeDeps();
const session = makeSession(true);
(session!.getElement as ReturnType<typeof vi.fn>).mockReturnValue({ children });
const sel = { hfId: "hf-abc" } as never;
const result = await sdkCutoverPersist(
sel,
[textOp("Hello world")],
"before",
"/comp.html",
session,
deps,
);
expect(result).toBe(false);
expect(session!.dispatch).not.toHaveBeenCalled();
expect(deps.writeProjectFile).not.toHaveBeenCalled();
});
it("dispatches setAttribute for attribute op with data- prefix", async () => {
const deps = makeDeps();
const session = makeSession(true);