import { describe, expect, it } from "@suaveplan/testing/runner"; import { SVG_COMPRESS_CODES, SvgCompressError, } from "../../error-codes/error-codes.js"; import { parseSvg, serializeSvg } from "./svg.js"; describe("parseSvg", () => { it("parses a minimal SVG element", { timeout: 5000 }, () => { const node = parseSvg(''); expect(node.type).toBe("element"); expect(node.name).toBe("svg"); expect(node.attributes).toEqual({ xmlns: "http://www.w3.org/2000/svg", }); expect(node.children).toEqual([]); }); it("parses self-closing elements", { timeout: 5000 }, () => { const node = parseSvg(''); expect(node.children).toHaveLength(1); const circle = node.children![0]!; expect(circle.type).toBe("element"); expect(circle.name).toBe("circle"); expect(circle.attributes).toEqual({ cx: "50", cy: "50", r: "40" }); expect(circle.children).toEqual([]); }); it("parses nested elements", { timeout: 5000 }, () => { const svg = ''; const node = parseSvg(svg); const g = node.children![0]!; expect(g.name).toBe("g"); const rect = g.children![0]!; expect(rect.name).toBe("rect"); expect(rect.attributes?.x).toBe("0"); }); it("parses text nodes", { timeout: 5000 }, () => { const node = parseSvg("Hello world"); const text = node.children![0]!; expect(text.name).toBe("text"); const textChild = text.children![0]!; expect(textChild.type).toBe("text"); expect(textChild.value).toBe("Hello world"); }); it("parses XML comments", { timeout: 5000 }, () => { const node = parseSvg(""); const comment = node.children![0]!; expect(comment.type).toBe("comment"); expect(comment.value).toBe(" a comment "); }); it("parses CDATA sections", { timeout: 5000 }, () => { const node = parseSvg("raw content"); const cdata = node.children![0]!; expect(cdata.type).toBe("cdata"); expect(cdata.value).toBe("raw content"); }); it("parses doctype declarations", { timeout: 5000 }, () => { const input = ''; const node = parseSvg(input); expect(node.type).toBe("element"); expect(node.name).toBe("svg"); }); it("parses XML declaration", { timeout: 5000 }, () => { const node = parseSvg( '' ); expect(node.name).toBe("svg"); }); it("parses attributes with single quotes", { timeout: 5000 }, () => { const node = parseSvg(""); const rect = node.children![0]!; expect(rect.attributes?.x).toBe("10"); expect(rect.attributes?.y).toBe("20"); }); it("parses attributes with no value (boolean)", { timeout: 5000 }, () => { const node = parseSvg(""); const use = node.children![0]!; expect(use.attributes?.disabled).toBe(""); }); it("parses multiple children", { timeout: 5000 }, () => { const node = parseSvg( "" ); expect(node.children).toHaveLength(3); }); it("parses whitespace-only text nodes", { timeout: 5000 }, () => { const node = parseSvg("\n \n"); const nonElement = node.children!.filter((c) => c.type !== "element"); expect(nonElement.every((n) => n.type === "text")).toBe(true); }); it("handles deeply nested structure", { timeout: 5000 }, () => { const svg = ""; const node = parseSvg(svg); const inner = node.children![0]!.children![0]!.children![0]!.children![0]!; expect(inner.name).toBe("rect"); }); it("throws PARSE_FAILED on unclosed tag", { timeout: 5000 }, () => { expect(() => parseSvg("")).toThrow(SvgCompressError); try { parseSvg(""); } catch (e) { expect((e as SvgCompressError).code).toBe( SVG_COMPRESS_CODES.PARSE_FAILED ); } }); it("throws PARSE_FAILED on mismatched closing tag", { timeout: 5000, }, () => { expect(() => parseSvg("")).toThrow(SvgCompressError); }); it("throws PARSE_FAILED on closing tag with empty stack", { timeout: 5000, }, () => { expect(() => parseSvg("")).toThrow(SvgCompressError); try { parseSvg(""); } catch (e) { expect((e as SvgCompressError).code).toBe( SVG_COMPRESS_CODES.PARSE_FAILED ); expect((e as SvgCompressError).message).toContain("none"); } }); it("throws PARSE_FAILED on empty input", { timeout: 5000 }, () => { expect(() => parseSvg("")).toThrow(SvgCompressError); }); it("throws PARSE_FAILED on non-SVG input (no root element)", { timeout: 5000, }, () => { expect(() => parseSvg("just text")).toThrow(SvgCompressError); }); it("parses attributes with namespace prefixes", { timeout: 5000 }, () => { const node = parseSvg( '' ); const use = node.children![0]!; expect(use.attributes?.["xlink:href"]).toBe("#id"); }); it("parses attribute values with encoded entities", { timeout: 5000, }, () => { const node = parseSvg(''); expect(node.attributes?.title).toBe("a & b"); }); it("parses multiple attributes on same element", { timeout: 5000 }, () => { const node = parseSvg( '' ); expect(node.attributes?.width).toBe("100"); expect(node.attributes?.height).toBe("200"); expect(node.attributes?.viewBox).toBe("0 0 100 200"); }); it("throws PARSE_FAILED on unclosed processing instruction", { timeout: 5000, }, () => { expect(() => parseSvg("")).toThrow( SvgCompressError ); }); it("throws PARSE_FAILED on unclosed comment", { timeout: 5000 }, () => { expect(() => parseSvg(""); const result = serializeSvg(node); expect(result).toContain(""); }); it("serializes CDATA nodes", { timeout: 5000 }, () => { const node = parseSvg("data"); const result = serializeSvg(node); expect(result).toContain(""); }); it("serializes elements with children (non-self-closing)", { timeout: 5000, }, () => { const node = parseSvg(""); const result = serializeSvg(node); expect(result).toContain(""); }); it("produces compact output (no extra whitespace)", { timeout: 5000, }, () => { const _node = parseSvg("\n \n"); const result = serializeSvg({ type: "element", name: "svg", attributes: {}, children: [ { type: "element", name: "rect", attributes: {}, children: [] }, ], }); expect(result).toBe(""); }); it("round-trips parse → serialize for complex SVG", { timeout: 5000, }, () => { const input = 'label'; const node = parseSvg(input); const output = serializeSvg(node); // Re-parse the output and check structure const reparsed = parseSvg(output); expect(reparsed.name).toBe("svg"); expect(reparsed.children).toHaveLength(1); const g = reparsed.children![0]!; expect(g.name).toBe("g"); expect(g.children).toHaveLength(2); }); it("serializes a text-type node directly", { timeout: 5000 }, () => { const textNode = { type: "text" as const, value: "hello" }; expect(serializeSvg(textNode)).toBe("hello"); }); it("serializes a doctype node", { timeout: 5000 }, () => { const doctype = { type: "doctype" as const, value: 'svg PUBLIC "-//W3C//DTD SVG 1.1//EN"', }; expect(serializeSvg(doctype)).toBe( '' ); }); it("omits undefined attributes", { timeout: 5000 }, () => { const node = serializeSvg({ type: "element", name: "svg", children: [], }); expect(node).toBe(""); }); });