import { describe, expect, it } from "vitest"; import { SUDOKU_DOCUMENT_SCHEMA, SUDOKU_DOCUMENT_VERSION, buildJpegPdf, renderPuzzleSvg, type SudokuDocument, } from "../../src/formats"; function visualDocument(): SudokuDocument { const values = Array.from({ length: 16 }, () => 0); values[0] = 1; values[15] = 4; const cornerMarks = Array.from({ length: 16 }, () => [] as number[]); cornerMarks[1] = [2, 3]; const centerMarks = Array.from({ length: 16 }, () => [] as number[]); centerMarks[2] = [1, 4]; return { schema: SUDOKU_DOCUMENT_SCHEMA, version: SUDOKU_DOCUMENT_VERSION, size: 4, givens: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], values, cornerMarks, centerMarks, colors: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], constraints: [ { type: "diagonal", direction: "main" }, { type: "killer-cage", cells: [0, 1], sum: 3 }, { type: "thermo", cells: [4, 5] }, { type: "arrow", bulb: [8], line: [9, 10] }, { type: "kropki", a: 2, b: 3, kind: "white" }, { type: "xv", a: 6, b: 7, total: 5 }, { type: "inequality", lesser: 10, greater: 11 }, { type: "renban", cells: [12, 13] }, { type: "palindrome", cells: [14, 15] }, { type: "maximum", cell: 5 }, { type: "quadruple", cells: [0, 1, 4, 5], digits: [1, 2] }, { type: "x-sum", side: "top", index: 0, sum: 1 }, { type: "skyscraper", side: "left", index: 2, count: 2 }, { type: "anti-knight" }, ], title: 'A < B & "C"', author: "Synthetic test", }; } describe("visual puzzle export", () => { it("renders a standalone SVG with clues, regions, givens and progress", () => { const svg = renderPuzzleSvg(visualDocument()); const parsed = new DOMParser().parseFromString(svg, "image/svg+xml"); expect(parsed.querySelector("parsererror")).toBeNull(); expect(parsed.documentElement.localName).toBe("svg"); expect(svg).toContain("A < B & "C""); expect(svg).toContain('class="constraint diagonal" x1="58" y1="132"'); expect(svg).toContain('class="constraint cage"'); expect(svg).toContain('class="constraint thermo"'); expect(svg).toContain('class="constraint arrow"'); expect(svg).toContain('class="constraint outside x-sum"'); expect(svg).toContain('class="constraint xv total-5"'); expect(svg).toContain(">5"); expect(svg).not.toContain(">V"); expect(svg).toContain('class="inequality-tip"'); expect(svg).toContain('class="cell-value given"'); expect(svg).toContain('class="cell-value progress"'); expect(svg).toContain('class="corner-note" x='); expect(svg).toContain('class="center-note" x='); expect(svg).not.toMatch(/<(?:script|image)|(?:href|src)=/iu); }); it("can omit all solving progress from the visual", () => { const svg = renderPuzzleSvg(visualDocument(), { includeProgress: false, includeNotes: false, }); expect(svg).toContain('class="cell-value given"'); expect(svg).not.toContain('class="cell-value progress"'); expect(svg).not.toContain('class="corner-note" x='); expect(svg).not.toContain('class="center-note" x='); expect(svg).not.toContain('class="cell-color"'); }); it("builds a bounded single-page PDF around local JPEG bytes", () => { const pdf = buildJpegPdf( new Uint8Array([0xff, 0xd8, 0xff, 0xd9]), 320, 240, ); const text = new TextDecoder("latin1").decode(pdf); expect(text.startsWith("%PDF-1.4")).toBe(true); expect(text).toContain("/DCTDecode"); expect(text).toContain("xref\n0 6"); expect(text.endsWith("%%EOF\n")).toBe(true); }); it("rejects invalid or unbounded PDF image input", () => { expect(() => buildJpegPdf(new Uint8Array([0, 1, 2, 3]), 320, 240)).toThrow( /JPEG/u, ); expect(() => buildJpegPdf(new Uint8Array([0xff, 0xd8, 0xff, 0xd9]), 99_999, 240), ).toThrow(/dimensions/u); }); it("validates progress before embedding it in a visual", () => { const document = visualDocument(); const invalidMarks = Array.from({ length: 16 }, () => [] as number[]); invalidMarks[0] = [99]; expect(() => renderPuzzleSvg({ ...document, cornerMarks: invalidMarks }), ).toThrow(/cornerMarks/u); }); });