import { describe, expect, it } from "vitest"; import type { AnimationDefinition } from "../../src/animation/animation.types"; import { animationStyle, withAnimationPreview, } from "../../src/animation/preview"; const fade: AnimationDefinition = { id: "fade", name: "Fade", targetNodeKey: "id:shape", property: "opacity", kind: "style", enabled: true, keyframes: [ { offset: 0, value: "0" }, { offset: 1, value: "1" }, ], timing: { durationMs: 500, delayMs: 20, iterations: "infinite", direction: "alternate", fillMode: "both", easing: "ease-in-out", }, }; describe("isolated animation preview", () => { it("creates project-side CSS targeting only projection metadata", () => { const css = animationStyle([fade]); expect(css).toContain("@keyframes svg-tools-animation-0"); expect(css).toContain('[data-svg-tools-node="id:shape"]'); expect(css).toContain("500ms ease-in-out 20ms infinite alternate both"); }); it("injects preview metadata without mutating the input projection", () => { const source = ''; const result = withAnimationPreview(source, [fade]); expect(source).not.toContain("data-svg-tools-preview"); expect(result).toContain('data-svg-tools-preview="animation"'); }); it("rejects unsafe properties and CSS rule or URL injection", () => { expect(() => animationStyle([{ ...fade, property: "background-image" }]), ).toThrow(/not application-safe/u); expect(() => animationStyle([ { ...fade, keyframes: [{ offset: 0, value: "url(https://evil.test)" }], }, ]), ).toThrow(/URLs or external resources/u); expect(() => animationStyle([ { ...fade, timing: { ...fade.timing, easing: "linear; color:red" }, }, ]), ).toThrow(/unsafe animation easing/u); }); it("omits disabled animations", () => { expect(animationStyle([{ ...fade, enabled: false }])).toBe(""); }); });