feat: expand sudoku analysis and interoperability
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user