feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -1,9 +1,77 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
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,
|
||||
@@ -112,6 +180,412 @@ describe("Sudoku board constraint visuals", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -164,6 +638,51 @@ describe("Sudoku board constraint visuals", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -215,4 +734,80 @@ describe("Sudoku board constraint visuals", () => {
|
||||
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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user