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("");
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("");
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("");
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("");
} 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("");
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("");
});
});