Files
sudoku-tools/tests/components/sudokuBoard.test.tsx
T

814 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { normalizePuzzle } from "../../src/domain";
import { SudokuBoard } from "../../src/components/SudokuBoard";
import { foggedCellsForPuzzle } from "../../src/components/fogVisibility";
describe("Sudoku board constraint visuals", () => {
it("orders retained underlays before semantic clues and overlays afterward", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
solution: [1, 2, 3, 4, 3, 4, 1, 2, 4, 3, 2, 1, 2, 1, 4, 3],
constraints: [
{ type: "diagonal", direction: "main" },
{ type: "fog", lights: [0], revealRadius: 0 },
],
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
visuals={[
{
type: "circle",
layer: "underlay",
center: { kind: "cell", cell: 0 },
radius: 0.3,
style: { fill: "#abcdef" },
},
{
type: "text",
layer: "overlay",
position: { kind: "cell", cell: 15 },
text: "overlay",
style: { fill: "#123456" },
},
]}
selected={new Set()}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
const underlay = container.querySelector(".safe-visual-layer--underlay")!;
const regions = container.querySelector(".region-boundaries")!;
const semantic = container.querySelector(".constraint-diagonal")!;
const overlay = container.querySelector(".safe-visual-layer--overlay")!;
const fogMask = container.querySelector(".fog-constraint-mask")!;
expect(
underlay.compareDocumentPosition(regions) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
underlay.compareDocumentPosition(semantic) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
semantic.compareDocumentPosition(overlay) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
overlay.compareDocumentPosition(fogMask) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(fogMask.querySelectorAll("rect").length).toBeGreaterThan(0);
expect(underlay.querySelector(".source-visual--circle")).toBeTruthy();
expect(overlay.querySelector(".source-visual--text")).toHaveTextContent(
"overlay",
);
});
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 Pack 1 cell, global and outside clues with distinct semantics", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
constraints: [
{ type: "maximum", cell: 4 },
{ type: "minimum", cell: 5, negated: true },
{ type: "odd", cell: 9 },
{ type: "even", cell: 10 },
{ type: "disjoint-groups" },
{
type: "little-killer",
side: "top",
index: 1,
direction: "down-right",
sum: 7,
},
{ type: "sandwich", side: "left", index: 2, sum: 3 },
],
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
selected={new Set()}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
const maximumPath = container
.querySelector(".constraint-maximum path")
?.getAttribute("d");
const minimumPath = container
.querySelector(".constraint-minimum path")
?.getAttribute("d");
expect(maximumPath).toBeTruthy();
expect(minimumPath).toBeTruthy();
expect(minimumPath).not.toBe(maximumPath);
expect(container.querySelector(".constraint-minimum")).toHaveClass(
"is-negated",
);
expect(
container.querySelector(".constraint-odd circle"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-even rect"),
).toBeInTheDocument();
expect(
container.querySelectorAll(".constraint-disjoint-groups path"),
).toHaveLength(4);
expect(
container.querySelector(
'.constraint-little-killer[data-direction="down-right"] .little-killer-arrow',
),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-little-killer text"),
).toHaveTextContent("7");
expect(
container.querySelector(".constraint-sandwich .outside-clue-kinds"),
).toHaveTextContent("1⋯N");
expect(
container.querySelector(".constraint-sandwich .outside-clue-value"),
).toHaveTextContent("3");
const board = screen.getByRole("grid", { name: "4 by 4 Sudoku grid" });
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"Disjoint groups rule: corresponding positions in every standard box contain each digit once.",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"little killer sum 7 from the top, column 2, travelling down right.",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"sandwich sum 3 from the left, row 3, between 1 and 4.",
),
);
expect(
screen.getByRole("gridcell", { name: "Row 3, column 2, empty" }),
).toHaveAccessibleDescription(
expect.stringMatching(/odd digit.*sandwich sum 3/iu),
);
});
it("renders Pack 2 lines and regions as distinct accessible clues", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
constraints: [
{ type: "between-line", cells: [0, 1, 2], negated: true },
{
type: "german-whisper",
cells: [4, 5, 6],
minimumDifference: 2,
},
{ type: "region-sum-line", cells: [8, 9, 10] },
{ type: "clone", cells: [0, 4], cloneCells: [3, 7] },
{ type: "extra-region", cells: [0, 3, 12, 15] },
],
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
selected={new Set()}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(container.querySelector(".constraint-between-line")).toHaveClass(
"is-negated",
);
expect(
container.querySelectorAll(".constraint-between-line > circle"),
).toHaveLength(2);
expect(
container.querySelector(".constraint-german-whisper polyline"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-region-sum-line polyline"),
).toBeInTheDocument();
expect(
container.querySelectorAll(
".constraint-region-sum-line .region-sum-divider",
),
).toHaveLength(1);
expect(
container.querySelectorAll(".constraint-clone .clone-cell-fill"),
).toHaveLength(4);
expect(
container.querySelectorAll(".constraint-clone .clone-boundary"),
).toHaveLength(2);
expect(
container.querySelector(".constraint-clone .clone-link"),
).toBeInTheDocument();
expect(
container.querySelectorAll(".constraint-clone .clone-label"),
).toHaveLength(2);
expect(
container.querySelectorAll(
".constraint-extra-region .extra-region-cell-fill",
),
).toHaveLength(4);
const board = screen.getByRole("grid", { name: "4 by 4 Sudoku grid" });
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"False clue: between line from row 1, column 1 through row 1, column 2 to row 1, column 3",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"German whisper through row 2, column 1; row 2, column 2; row 2, column 3; adjacent digits differ by at least 2.",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"clone regions pairing row 1, column 1; row 2, column 1 with row 1, column 4; row 2, column 4 in order.",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"Extra region through row 1, column 1; row 1, column 4; row 4, column 1; row 4, column 4; every digit appears exactly once.",
),
);
expect(
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" }),
).toHaveAccessibleDescription(
expect.stringMatching(/between line.*clone regions.*extra region/iu),
);
});
it("renders Pack 3 pattern lines and indexers with distinct non-colour shapes", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 6,
givens: new Array<number>(36).fill(0),
constraints: [
{ type: "modular-line", cells: [0, 1, 2], negated: true },
{ type: "entropic-line", cells: [6, 7, 8] },
{ type: "zipper-line", cells: [12, 13, 14, 15, 16] },
{ type: "double-arrow", cells: [18, 19, 20] },
{ type: "indexer", kind: "row", cell: 24 },
{ type: "indexer", kind: "column", cell: 25 },
{ type: "indexer", kind: "box", cell: 26 },
],
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
selected={new Set()}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(container.querySelector(".constraint-modular-line")).toHaveClass(
"is-negated",
);
expect(
container.querySelectorAll(".constraint-modular-line .modular-line-node"),
).toHaveLength(3);
expect(
container.querySelector(
".constraint-entropic-line .entropic-line-underlay",
),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-entropic-line .entropic-line-path"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-zipper-line .zipper-line-centre"),
).toBeInTheDocument();
expect(
container.querySelectorAll(".constraint-double-arrow > circle"),
).toHaveLength(2);
expect(
container.querySelectorAll(
".constraint-double-arrow .double-arrow-chevron",
),
).toHaveLength(2);
expect(
container.querySelector(".constraint-indexer--row circle"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-indexer--column rect"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-indexer--box rect"),
).toBeInTheDocument();
expect(
container.querySelector(".constraint-indexer--row text"),
).toHaveTextContent("R");
expect(
container.querySelector(".constraint-indexer--column text"),
).toHaveTextContent("C");
expect(
container.querySelector(".constraint-indexer--box text"),
).toHaveTextContent("B");
const board = screen.getByRole("grid", { name: "6 by 6 Sudoku grid" });
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"False clue: modular line through row 1, column 1; row 1, column 2; row 1, column 3",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"entropic line through row 2, column 1; row 2, column 2; row 2, column 3",
),
);
expect(board).toHaveAccessibleDescription(
expect.stringContaining(
"row indexer at row 5, column 1; the marker digit selects a row in the same column",
),
);
});
it("masks fogged content and clues until a correct entry reveals them", () => {
const solution = [1, 2, 3, 4, 3, 4, 1, 2, 2, 1, 4, 3, 4, 3, 2, 1];
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
solution,
constraints: [
{ type: "fog", lights: [0], revealRadius: 1 },
{ type: "even", cell: 10 },
],
});
const pointerDown = vi.fn();
const keyDown = vi.fn();
const wrongValues = new Array<number>(16).fill(0);
wrongValues[5] = 2;
wrongValues[10] = 3;
const { container, rerender } = render(
<SudokuBoard
puzzle={puzzle}
values={wrongValues}
candidates={new Array<number>(16).fill(15)}
selected={new Set([10])}
highlighted={new Set([10])}
conflicts={new Set([10])}
activeCell={1}
candidateOverlay={{
activeValues: [4],
candidateCells: [0, 10],
links: [
{
id: "hidden-link",
kind: "strong",
a: { cell: 0, value: 4 },
b: { cell: 10, value: 4 },
contexts: [{ kind: "house", label: "Test" }],
},
],
}}
guidedHintOverlay={{
focusCells: [10],
placements: [{ cell: 10, value: 4 }],
}}
onCellPointerDown={pointerDown}
onCellPointerEnter={vi.fn()}
onKeyDown={keyDown}
/>,
);
const hidden = screen.getByRole("gridcell", {
name: "Row 3, column 3, obscured by fog",
});
expect(hidden).toBeDisabled();
expect(hidden).toHaveClass("is-fogged");
expect(hidden).not.toHaveClass(
"is-selected",
"has-conflict",
"is-hint-focus",
);
expect(hidden).toHaveAccessibleDescription(
"Obscured by Fog of War. This cell cannot be selected until revealed.",
);
expect(hidden).toBeEmptyDOMElement();
expect(container.querySelector(".constraint-even")).not.toBeInTheDocument();
expect(
container.querySelectorAll(".fog-constraint-mask rect"),
).toHaveLength(12);
expect(
container.querySelector(".candidate-link-layer"),
).not.toBeInTheDocument();
expect(
screen.getByRole("grid", { name: "4 by 4 Sudoku grid" }),
).not.toHaveAccessibleDescription(expect.stringContaining("even digit"));
fireEvent.pointerDown(hidden);
expect(pointerDown).not.toHaveBeenCalled();
fireEvent.pointerDown(
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" }),
);
expect(pointerDown).toHaveBeenCalledWith(0, expect.anything());
fireEvent.keyDown(
screen.getByRole("gridcell", { name: "Row 1, column 2, empty" }),
{ key: "ArrowRight" },
);
fireEvent.keyDown(
screen.getByRole("gridcell", { name: "Row 1, column 2, empty" }),
{ key: "a", ctrlKey: true },
);
expect(keyDown).not.toHaveBeenCalled();
const correctValues = new Array<number>(16).fill(0);
correctValues[5] = 4;
rerender(
<SudokuBoard
puzzle={puzzle}
values={correctValues}
selected={new Set()}
activeCell={1}
onCellPointerDown={pointerDown}
onCellPointerEnter={vi.fn()}
onKeyDown={keyDown}
/>,
);
expect(
screen.getByRole("gridcell", { name: "Row 3, column 3, empty" }),
).toHaveAccessibleDescription(expect.stringContaining("even digit"));
expect(container.querySelector(".constraint-even")).toBeInTheDocument();
expect(
container.querySelectorAll(".fog-constraint-mask rect"),
).toHaveLength(7);
});
it("reveals given cells and their Chebyshev neighbourhood under fog", () => {
const solution = [1, 2, 3, 4, 3, 4, 1, 2, 2, 1, 4, 3, 4, 3, 2, 1];
const givens = new Array<number>(16).fill(0);
givens[15] = 1;
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens,
solution,
constraints: [{ type: "fog", lights: [0], revealRadius: 1 }],
});
const fogged = foggedCellsForPuzzle(puzzle, givens);
expect(fogged.has(10)).toBe(false);
expect(fogged.has(15)).toBe(false);
expect(fogged.has(9)).toBe(true);
});
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("keeps guided effects distinct from focus cells and exposes the preview", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
centerMarks={[0, 15, 15, ...new Array<number>(13).fill(0)]}
selected={new Set()}
activeCell={0}
guidedHintOverlay={{
focusCells: [0, 1, 2],
placements: [{ cell: 0, value: 4 }],
eliminations: [{ cell: 1, values: [2, 3] }],
}}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(container.querySelectorAll(".is-hint-focus")).toHaveLength(3);
expect(container.querySelectorAll(".is-hint-placement")).toHaveLength(1);
expect(container.querySelectorAll(".is-hint-elimination")).toHaveLength(1);
expect(
container.querySelector(".hint-placement-preview"),
).toHaveTextContent("4");
expect(
container.querySelector(".hint-elimination-preview"),
).toHaveTextContent("23");
expect(
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" }),
).toHaveAccessibleDescription(
expect.stringContaining("hint preview: place 4"),
);
expect(
screen.getByRole("gridcell", { name: "Row 1, column 2, empty" }),
).toHaveAccessibleDescription(
expect.stringContaining("hint preview: remove 2, 3 from the candidates"),
);
});
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");
});
it("offers off, concise and detailed screen-reader candidate output", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
});
const baseProps = {
puzzle,
values: puzzle.givens,
cornerMarks: [3, ...new Array<number>(15).fill(0)],
centerMarks: [12, ...new Array<number>(15).fill(0)],
selected: new Set([0]),
activeCell: 0,
onCellPointerDown: vi.fn(),
onCellPointerEnter: vi.fn(),
onKeyDown: vi.fn(),
} as const;
const { rerender } = render(
<SudokuBoard {...baseProps} candidateVerbosity="detailed" />,
);
const cell = () =>
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" });
expect(cell()).toHaveAccessibleDescription(
expect.stringContaining("corner notes 1, 2"),
);
expect(cell()).toHaveAccessibleDescription(
expect.stringContaining("centre notes 3, 4"),
);
rerender(<SudokuBoard {...baseProps} candidateVerbosity="concise" />);
expect(cell()).toHaveAccessibleDescription(
expect.stringContaining("2 corner notes"),
);
expect(cell()).toHaveAccessibleDescription(
expect.stringContaining("2 centre notes"),
);
expect(cell()).not.toHaveAccessibleDescription(
expect.stringContaining("corner notes 1, 2"),
);
rerender(<SudokuBoard {...baseProps} candidateVerbosity="off" />);
expect(cell()).not.toHaveAccessibleDescription(
expect.stringMatching(/candidate|corner note|centre note/iu),
);
});
it("adds a non-colour pattern layer and names the pattern", () => {
const puzzle = normalizePuzzle({
version: 1,
size: 4,
givens: new Array<number>(16).fill(0),
});
const { container } = render(
<SudokuBoard
puzzle={puzzle}
values={puzzle.givens}
colors={[1, ...new Array<number>(15).fill(0)]}
selected={new Set()}
activeCell={0}
onCellPointerDown={vi.fn()}
onCellPointerEnter={vi.fn()}
onKeyDown={vi.fn()}
/>,
);
expect(
container.querySelector(".has-color-1 .cell-color-pattern"),
).toBeInTheDocument();
expect(
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" }),
).toHaveAccessibleDescription(
expect.stringContaining("colour 1: red, diagonal stripes"),
);
});
});