118 lines
4.2 KiB
TypeScript
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 < 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</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);
|
|
});
|
|
});
|