219 lines
6.8 KiB
TypeScript
219 lines
6.8 KiB
TypeScript
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";
|
|
|
|
describe("Sudoku board constraint visuals", () => {
|
|
it("distinguishes XV sums from directional inequalities", () => {
|
|
const puzzle = normalizePuzzle({
|
|
version: 1,
|
|
size: 4,
|
|
givens: new Array<number>(16).fill(0),
|
|
constraints: [
|
|
{ type: "xv", a: 0, b: 1, total: 5 },
|
|
{ type: "xv", a: 4, b: 5, total: 10 },
|
|
{ type: "inequality", lesser: 8, greater: 9 },
|
|
],
|
|
});
|
|
|
|
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(".constraint-xv--5 text")).toHaveTextContent(
|
|
"5",
|
|
);
|
|
expect(
|
|
container.querySelector(".constraint-xv--10 text"),
|
|
).toHaveTextContent("10");
|
|
expect(
|
|
container.querySelector(".constraint-inequality path"),
|
|
).toHaveAttribute("d", "M0.12 -0.17L-0.12 0L0.12 0.17");
|
|
expect(
|
|
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");
|
|
});
|
|
});
|