Files
sudoku-tools/tests/formats/visual.test.ts
T

118 lines
4.2 KiB
TypeScript

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 &lt; B &amp; &quot;C&quot;");
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</text>");
expect(svg).not.toContain(">V</text>");
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);
});
});