feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -2,6 +2,8 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { Workbench } from "../../src/components/Workbench";
|
||||
import { encodePuzzleHash, fromDomainPuzzle } from "../../src/formats";
|
||||
import { SUDOKU_DOCUMENT_SCHEMA } from "../../src/formats";
|
||||
|
||||
class WorkerStub {
|
||||
addEventListener() {}
|
||||
@@ -63,6 +65,37 @@ describe("Sudoku workbench", () => {
|
||||
expect(undo).toBeDisabled();
|
||||
});
|
||||
|
||||
it("fills a tracked candidate grid as one undoable maintenance action", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
|
||||
expect(
|
||||
screen.getByText("Candidate tracking is currently inactive."),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Fill legal candidates" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("The guided candidate grid is active."),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("gridcell", {
|
||||
name: "Row 1, column 3, empty",
|
||||
}),
|
||||
).toHaveAccessibleDescription(expect.stringContaining("centre notes"));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Undo" }));
|
||||
expect(
|
||||
screen.getByText("Candidate tracking is currently inactive."),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("gridcell", {
|
||||
name: "Row 1, column 3, empty",
|
||||
}),
|
||||
).not.toHaveAccessibleDescription(expect.stringContaining("centre notes"));
|
||||
});
|
||||
|
||||
it("supports non-wrapping WAI-ARIA grid navigation", () => {
|
||||
render(<Workbench />);
|
||||
|
||||
@@ -91,6 +124,94 @@ describe("Sudoku workbench", () => {
|
||||
expect(cell(1, 1)).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
it("offers tap-by-tap multi-selection without drag selection", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
const grid = screen.getByRole("grid", { name: "9 by 9 Sudoku grid" });
|
||||
const cell = (column: number) =>
|
||||
within(grid).getByRole("gridcell", {
|
||||
name: new RegExp(`^Row 1, column ${String(column)},`, "u"),
|
||||
});
|
||||
|
||||
fireEvent.pointerDown(cell(3), { buttons: 1 });
|
||||
const toggleMode = screen.getByRole("button", { name: "Tap multi-select" });
|
||||
expect(toggleMode).toHaveAttribute("aria-pressed", "false");
|
||||
await user.click(toggleMode);
|
||||
expect(toggleMode).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
fireEvent.pointerDown(cell(4), { buttons: 1 });
|
||||
expect(cell(3)).toHaveAttribute("aria-selected", "true");
|
||||
expect(cell(4)).toHaveAttribute("aria-selected", "true");
|
||||
fireEvent.pointerEnter(cell(5), { buttons: 1 });
|
||||
expect(cell(5)).toHaveAttribute("aria-selected", "false");
|
||||
|
||||
fireEvent.pointerDown(cell(3), { buttons: 1 });
|
||||
expect(cell(3)).toHaveAttribute("aria-selected", "false");
|
||||
expect(cell(4)).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
|
||||
it("renders one sticky entry pad in a narrow viewport", () => {
|
||||
const original = Object.getOwnPropertyDescriptor(window, "matchMedia");
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
value: (query: string) => ({
|
||||
matches: query === "(max-width: 48rem)",
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}),
|
||||
});
|
||||
|
||||
const { container, unmount } = render(<Workbench />);
|
||||
expect(container.querySelectorAll(".mobile-number-pad")).toHaveLength(1);
|
||||
expect(screen.getAllByRole("group", { name: "Entry mode" })).toHaveLength(
|
||||
1,
|
||||
);
|
||||
unmount();
|
||||
if (original === undefined) Reflect.deleteProperty(window, "matchMedia");
|
||||
else Object.defineProperty(window, "matchMedia", original);
|
||||
});
|
||||
|
||||
it("keeps fogged selections out of toolbar and helper output", async () => {
|
||||
const user = userEvent.setup();
|
||||
const previousHash = window.location.hash;
|
||||
window.location.hash = encodePuzzleHash(
|
||||
fromDomainPuzzle({
|
||||
version: 1,
|
||||
size: 4,
|
||||
givens: new Array<number>(16).fill(0),
|
||||
solution: [1, 2, 3, 4, 3, 4, 1, 2, 2, 1, 4, 3, 4, 3, 2, 1],
|
||||
constraints: [{ type: "fog", lights: [15], revealRadius: 0 }],
|
||||
}),
|
||||
);
|
||||
const { unmount } = render(<Workbench />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("gridcell", {
|
||||
name: "Row 1, column 1, obscured by fog",
|
||||
}),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByText("r4c4")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Helpers" }));
|
||||
await user.click(screen.getByRole("tab", { name: "Sum Lab" }));
|
||||
await user.clear(screen.getByLabelText("Target sum"));
|
||||
await user.type(screen.getByLabelText("Target sum"), "4");
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", {
|
||||
name: "Use 1 board cell and candidates",
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText(/^r4c4:/u)).toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
window.location.hash = previousHash;
|
||||
});
|
||||
|
||||
it("shows digit progress and toggles matching-digit highlights separately from selection", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
@@ -257,8 +378,8 @@ describe("Sudoku workbench", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(container.querySelectorAll(".is-negated")).toHaveLength(2);
|
||||
|
||||
await user.clear(screen.getByLabelText("Clue"));
|
||||
await user.type(screen.getByLabelText("Clue"), "6562");
|
||||
await user.clear(screen.getByLabelText("Sum"));
|
||||
await user.type(screen.getByLabelText("Sum"), "6562");
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add / replace outside clue" }),
|
||||
).toBeDisabled();
|
||||
@@ -375,4 +496,69 @@ describe("Sudoku workbench", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps imported visuals, provenance and metadata after a board edit", async () => {
|
||||
const user = userEvent.setup();
|
||||
let downloaded: Blob | undefined;
|
||||
vi.mocked(URL.createObjectURL).mockImplementation((value) => {
|
||||
downloaded = value as Blob;
|
||||
return "blob:preserved-workbench-export";
|
||||
});
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
|
||||
render(<Workbench />);
|
||||
|
||||
const imported = {
|
||||
schema: SUDOKU_DOCUMENT_SCHEMA,
|
||||
version: 1,
|
||||
size: 4,
|
||||
givens: Array<number>(16).fill(0),
|
||||
constraints: [],
|
||||
title: "Preservation regression",
|
||||
visuals: [
|
||||
{
|
||||
type: "text",
|
||||
layer: "overlay",
|
||||
position: { kind: "cell", cell: 0 },
|
||||
text: "source label",
|
||||
style: { fill: "#123456" },
|
||||
},
|
||||
],
|
||||
source: { format: "sudokupad", id: "original-source" },
|
||||
metadata: { edition: "kept" },
|
||||
};
|
||||
await user.click(screen.getByRole("button", { name: "Import / export" }));
|
||||
fireEvent.change(screen.getByPlaceholderText(/Paste 81 characters/u), {
|
||||
target: { value: JSON.stringify(imported) },
|
||||
});
|
||||
await user.click(screen.getByRole("button", { name: "Import locally" }));
|
||||
expect(document.querySelector(".source-visual--text")).toHaveTextContent(
|
||||
"source label",
|
||||
);
|
||||
|
||||
fireEvent.pointerDown(
|
||||
screen.getByRole("gridcell", { name: "Row 1, column 1, empty" }),
|
||||
{ buttons: 1 },
|
||||
);
|
||||
await user.click(
|
||||
within(screen.getByRole("group", { name: "Digits" })).getByRole(
|
||||
"button",
|
||||
{ name: "1" },
|
||||
),
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Import / export" }));
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Download project JSON" }),
|
||||
);
|
||||
|
||||
const exported = JSON.parse((await downloaded?.text()) ?? "{}") as {
|
||||
values?: number[];
|
||||
visuals?: unknown[];
|
||||
source?: unknown;
|
||||
metadata?: unknown;
|
||||
};
|
||||
expect(exported.values?.[0]).toBe(1);
|
||||
expect(exported.visuals).toEqual(imported.visuals);
|
||||
expect(exported.source).toEqual(imported.source);
|
||||
expect(exported.metadata).toEqual(imported.metadata);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user