feat: expand sudoku analysis and interoperability

This commit is contained in:
2026-08-30 23:21:56 +02:00
parent 4a9869baa0
commit 8ca9300ab3
73 changed files with 12482 additions and 384 deletions
+175 -1
View File
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { normalizePuzzle } from "../../src/domain";
import { SudokuBoard } from "../../src/components/SudokuBoard";
@@ -41,4 +41,178 @@ describe("Sudoku board constraint visuals", () => {
container.querySelector(".constraint-inequality-tip"),
).toBeInTheDocument();
});
it("renders false local clues and combines dual-purpose outside labels", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
constraints: [
{ type: "maximum", cell: 5, negated: true },
{ type: "thermo", cells: [8, 9], negated: true },
{
type: "quadruple",
cells: [0, 1, 4, 5],
digits: [1, 3],
negated: true,
},
{
type: "x-sum",
side: "top",
index: 0,
sum: 42,
negated: true,
},
{
type: "skyscraper",
side: "top",
index: 0,
count: 42,
negated: true,
},
],
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
selected={new Set([0])}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(container.querySelector(".sudoku-board-frame")).toHaveClass(
"has-outside-clues",
);
expect(container.querySelectorAll(".constraint-outside")).toHaveLength(1);
expect(container.querySelector(".outside-clue-kinds")).toHaveTextContent(
"Σ · ▥",
);
expect(container.querySelector(".outside-clue-value")).toHaveTextContent(
"≠42",
);
expect(container.querySelectorAll(".is-negated")).toHaveLength(4);
expect(
container.querySelector(".constraint-thermo.is-negated polyline"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-thermo .constraint-false-mark"),
).toHaveTextContent("≠");
const board = screen.getByRole("grid", { name: "4 by 4 Sudoku grid" });
expect(board).toHaveAccessibleDescription(
expect.stringContaining("False clue: X-sum 42 from the top, column 1."),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining("False clue: maximum at row 2, column 2."),
);
});
it("renders candidate filters and strong/weak graph overlays", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
selected={new Set([0])}
activeCell={0}
candidateOverlay={{
activeValues: [1],
candidateCells: [0, 1],
links: [
{
id: "strong",
kind: "strong",
a: { cell: 0, value: 1 },
b: { cell: 1, value: 1 },
contexts: [{ kind: "house", label: "Row 1" }],
},
{
id: "weak",
kind: "weak",
a: { cell: 0, value: 2 },
b: { cell: 0, value: 3 },
contexts: [{ kind: "cell", label: "Cell r1c1" }],
},
],
}}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(
container.querySelectorAll(".is-candidate-highlighted"),
).toHaveLength(2);
expect(
container.querySelectorAll(".candidate-overlay-link--strong"),
).toHaveLength(1);
expect(
container.querySelectorAll(".candidate-overlay-link--weak"),
).toHaveLength(1);
expect(container.querySelectorAll(".candidate-overlay-node")).toHaveLength(
4,
);
});
it("exposes real row semantics and detailed per-cell state", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
constraints: [{ type: "killer-cage", cells: [0, 1], sum: 3 }],
});
render(
<SudokuBoard
puzzle={puzzle}
values={[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
cornerMarks={[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
centerMarks={new Array<number>(16).fill(0)}
candidates={[0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
colors={[0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
selected={new Set([1])}
conflicts={new Set([1])}
activeCell={1}
showCandidates
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
const board = screen.getByRole("grid", { name: "4 by 4 Sudoku grid" });
expect(board).toHaveAttribute("aria-rowcount", "4");
expect(board).toHaveAttribute("aria-colcount", "4");
expect(board).toHaveAccessibleDescription(
expect.stringContaining("Home and End move to the first and last cell"),
);
expect(screen.getAllByRole("row")).toHaveLength(4);
const given = screen.getByRole("gridcell", {
name: "Row 1, column 1, 1",
});
expect(given).toHaveAttribute("aria-readonly", "true");
expect(given).toHaveAccessibleDescription(
expect.stringMatching(/region 1.*given digit.*killer cage 3/i),
);
const annotated = screen.getByRole("gridcell", {
name: "Row 1, column 2, empty",
});
expect(annotated).toHaveAccessibleDescription(
expect.stringMatching(
/region 1.*corner notes 1.*automatic candidates 1, 2.*colour 2.*conflict/i,
),
);
expect(annotated).toHaveAttribute("aria-colindex", "2");
expect(annotated).toHaveAttribute("aria-invalid", "true");
expect(annotated).toHaveAttribute("aria-selected", "true");
});
});