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