import { describe, expect, it } from "@effect/vitest"; import { ARTIFACT_PREVIEW_MARKUP_LIMIT, sanitizeArtifactPreviewMarkup } from "./artifact-preview"; describe("sanitizeArtifactPreviewMarkup", () => { describe("the shape it is written against", () => { it("keeps the structure and classes a smoke render produces", () => { const result = sanitizeArtifactPreviewMarkup( '

Revenue

' + '
', ); expect(result).toBe( '

Revenue

' + '
', ); }); /** * The app-layout pattern the `artifact-style` skill teaches — a bounded flex * column with one scrolling region under a sticky header — is expressed * ENTIRELY in class names. A sanitizer that dropped or rewrote them would * leave the gallery card showing a layout the artifact does not have, so the * classes that carry the structure are pinned here. */ it("keeps the app-layout classes a bounded artifact is built from", () => { const result = sanitizeArtifactPreviewMarkup( '
Projects
' + '
' + '' + "
Name
", ); expect(result).toContain('class="flex h-full flex-col gap-4"'); expect(result).toContain('class="min-h-0 flex-1 overflow-auto"'); expect(result).toContain('class="sticky top-0 z-10 bg-background"'); }); it("keeps a chart's svg geometry", () => { const result = sanitizeArtifactPreviewMarkup( '', ); expect(result).toContain('viewbox="0 0 100 40"'); expect(result).toContain('d="M0,10 L20,5 L40,18"'); expect(result).toContain('stroke-width="1.5"'); }); it("keeps table composition", () => { const result = sanitizeArtifactPreviewMarkup( '' + "
Name
Row
", ); expect(result).toBe( '' + "
Name
Row
", ); }); it("keeps React's HTML comment markers out without eating the content around them", () => { // `renderToReadableStream` emits ``/`` around Suspense // boundaries. They are comments, so they go; their content stays. const result = sanitizeArtifactPreviewMarkup( "
Loading
", ); expect(result).toBe("
Loading
"); }); }); describe("scripts", () => { it("removes a script element AND its body", () => { const result = sanitizeArtifactPreviewMarkup( '
beforeafter
', ); expect(result).toBe("
beforeafter
"); expect(result).not.toContain("fetch"); expect(result).not.toContain("evil.test"); }); it("removes a style element AND its body", () => { const result = sanitizeArtifactPreviewMarkup( "
ok
", ); expect(result).toBe("
ok
"); expect(result).not.toContain("evil.test"); }); it("does not let a nested tag close the skip early", () => { const result = sanitizeArtifactPreviewMarkup( "
after
", ); expect(result).toBe("
after
"); expect(result).not.toContain("still inside"); expect(result).not.toContain("payload"); }); it("removes noscript, template and object with their contents", () => { for (const tag of ["noscript", "template", "object"]) { const result = sanitizeArtifactPreviewMarkup(`
<${tag}>hiddenkept
`); expect(result).toBe("
kept
"); } }); }); describe("event handlers", () => { it("strips every on* attribute", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); expect(result).not.toContain("steal"); }); it("strips handlers whatever their casing", () => { const result = sanitizeArtifactPreviewMarkup('
x
'); expect(result).not.toContain("steal"); }); it("strips a handler whose value contains a closing angle bracket", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); expect(result).not.toContain("steal"); }); }); describe("external references", () => { it("drops img entirely, so no request is ever made", () => { const result = sanitizeArtifactPreviewMarkup( '
ok
', ); expect(result).toBe("
ok
"); expect(result).not.toContain("evil.test"); }); it("drops iframe and its content", () => { const result = sanitizeArtifactPreviewMarkup( '
ok
', ); expect(result).toBe("
ok
"); }); it("unwraps an anchor but keeps its text, dropping the href", () => { const result = sanitizeArtifactPreviewMarkup( '
Dashboard
', ); expect(result).toBe("
Dashboard
"); expect(result).not.toContain("evil.test"); }); it("drops a style declaration that can fetch", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); expect(result).not.toContain("evil.test"); }); it("drops a url() smuggled into an allowed style property", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); expect(result).not.toContain("evil.test"); }); it("drops position and transform, which would escape the card", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); }); it("drops an svg url() paint that points anywhere but a local id", () => { const result = sanitizeArtifactPreviewMarkup( '', ); expect(result).toContain(" { const result = sanitizeArtifactPreviewMarkup( '
ok
', ); expect(result).toBe("
ok
"); }); it("neutralises a javascript: scheme wherever it lands", () => { const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).not.toContain("javascript:"); }); }); describe("interactive elements", () => { it("unwraps a button, keeping its label", () => { const result = sanitizeArtifactPreviewMarkup("
"); expect(result).toBe("
Refresh
"); }); it("drops form controls", () => { const result = sanitizeArtifactPreviewMarkup( '
ok
', ); expect(result).toBe("
ok
"); expect(result).not.toContain("evil.test"); }); }); describe("ids", () => { it("namespaces ids so a preview cannot collide with the host document", () => { const result = sanitizeArtifactPreviewMarkup('
x
'); expect(result).toBe('
x
'); }); it("rewrites local paint references in step with the ids", () => { const result = sanitizeArtifactPreviewMarkup( '' + '', ); expect(result).toContain('id="artifact-preview-grad"'); expect(result).toContain('fill="url(#artifact-preview-grad)"'); }); }); describe("text and entities", () => { it("keeps escaped markup escaped, and does not let it re-enter as an element", () => { const result = sanitizeArtifactPreviewMarkup( "
<script>alert(1)</script>
", ); expect(result).toBe("
<script>alert(1)</script>
"); expect(result).not.toContain(" { // The input is already-escaped HTML. Escaping `&` a second time would // turn `&` into `&amp;` and corrupt every Tailwind arbitrary // variant in the markup. const once = sanitizeArtifactPreviewMarkup("
a < b && c
"); expect(once).toBe("
a < b && c
"); expect(sanitizeArtifactPreviewMarkup(once ?? "")).toBe(once); }); it("preserves a Tailwind arbitrary variant containing an ampersand", () => { // What React writes for `[&_.recharts-layer]:outline-hidden`. const result = sanitizeArtifactPreviewMarkup( '
x
', ); expect(result).toBe('
x
'); expect(result).not.toContain("&amp;"); }); }); describe("size", () => { it("returns null past the cap", () => { const huge = `
${"x".repeat(ARTIFACT_PREVIEW_MARKUP_LIMIT + 1)}
`; expect(sanitizeArtifactPreviewMarkup(huge)).toBeNull(); }); it("keeps markup just under the cap", () => { const body = "x".repeat(1000); expect(sanitizeArtifactPreviewMarkup(`
${body}
`)).toBe(`
${body}
`); }); }); describe("nothing usable", () => { it("returns null for empty input", () => { expect(sanitizeArtifactPreviewMarkup("")).toBeNull(); }); it("returns null when everything sanitized away", () => { expect(sanitizeArtifactPreviewMarkup("")).toBeNull(); }); it("returns null for whitespace-only structure", () => { expect(sanitizeArtifactPreviewMarkup("
")).toBeNull(); }); it("keeps a purely geometric render that has no text at all", () => { // A chart-only artifact is a real preview even with zero text nodes. const result = sanitizeArtifactPreviewMarkup( '
', ); expect(result).not.toBeNull(); expect(result).toContain(" { it("does not hang or throw on an unterminated tag", () => { expect(() => sanitizeArtifactPreviewMarkup('
{ expect(() => sanitizeArtifactPreviewMarkup("