fix: stabilize Sudoku workspace layout
This commit is contained in:
@@ -65,6 +65,95 @@ test("loads standalone and keeps the core play workflow local", async ({
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("keeps the board, keypad and solver controls within their layouts", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 });
|
||||
await page.goto("/deep/nested/sudoku/");
|
||||
|
||||
const frame = page.locator(".sudoku-board-frame");
|
||||
const sidebar = page.locator(".side-panel");
|
||||
const boardColumn = page.locator(".board-column");
|
||||
const scroller = page.locator(".board-viewport__scroller");
|
||||
await expect(frame).toBeVisible();
|
||||
|
||||
const fittedWidth = (await frame.boundingBox())?.width ?? 0;
|
||||
expect(fittedWidth).toBeGreaterThan(0);
|
||||
expect(
|
||||
await scroller.evaluate(
|
||||
(element) => element.scrollHeight <= element.clientHeight + 1,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const digits = page.getByRole("group", { name: "Digits" });
|
||||
const digitBoxes = await Promise.all(
|
||||
["1", "2", "3", "4", "7", "9"].map(async (digit) =>
|
||||
digits.getByRole("button", { name: digit, exact: true }).boundingBox(),
|
||||
),
|
||||
);
|
||||
const [one, two, three, four, seven, nine] = digitBoxes;
|
||||
const erase = await digits
|
||||
.getByRole("button", { name: "Erase" })
|
||||
.boundingBox();
|
||||
for (const box of [...digitBoxes, erase]) expect(box).not.toBeNull();
|
||||
expect(Math.abs(one!.y - two!.y)).toBeLessThan(1);
|
||||
expect(Math.abs(two!.y - three!.y)).toBeLessThan(1);
|
||||
expect(Math.abs(one!.x - four!.x)).toBeLessThan(1);
|
||||
expect(Math.abs(four!.x - seven!.x)).toBeLessThan(1);
|
||||
expect(four!.y).toBeGreaterThan(one!.y + one!.height);
|
||||
expect(seven!.y).toBeGreaterThan(four!.y + four!.height);
|
||||
expect(Math.abs(erase!.x - one!.x)).toBeLessThan(1);
|
||||
expect(
|
||||
Math.abs(erase!.x + erase!.width - (three!.x + three!.width)),
|
||||
).toBeLessThan(1);
|
||||
expect(erase!.y).toBeGreaterThan(nine!.y + nine!.height);
|
||||
|
||||
await page.getByRole("button", { name: "Zoom board out" }).click();
|
||||
await expect(page.getByLabel("Board zoom level")).toHaveText("75%");
|
||||
const reducedWidth = (await frame.boundingBox())?.width ?? 0;
|
||||
expect(reducedWidth / fittedWidth).toBeCloseTo(0.75, 2);
|
||||
await page.getByRole("button", { name: "Fit board" }).click();
|
||||
|
||||
const baselineSidebarWidth = (await sidebar.boundingBox())?.width ?? 0;
|
||||
const baselineBoardWidth = (await boardColumn.boundingBox())?.width ?? 0;
|
||||
for (const workspace of ["Set", "Generate", "Solve", "Helpers"]) {
|
||||
await page.getByRole("button", { name: workspace, exact: true }).click();
|
||||
expect((await sidebar.boundingBox())?.width ?? 0).toBeCloseTo(
|
||||
baselineSidebarWidth,
|
||||
1,
|
||||
);
|
||||
expect((await boardColumn.boundingBox())?.width ?? 0).toBeCloseTo(
|
||||
baselineBoardWidth,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
await page.getByRole("tab", { name: "Candidate links" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Links and houses" }),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
await sidebar.evaluate(
|
||||
(element) => element.scrollWidth <= element.clientWidth + 1,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "Solve", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Build logical solve path" }).click();
|
||||
const solveSteps = page.locator(".solve-step");
|
||||
await expect(solveSteps.first()).toBeVisible({ timeout: 60_000 });
|
||||
expect(await solveSteps.count()).toBeGreaterThan(0);
|
||||
expect(
|
||||
await solveSteps.evaluateAll((steps) =>
|
||||
steps.every(
|
||||
(step) =>
|
||||
step.scrollHeight <= step.clientHeight + 1 &&
|
||||
step.scrollWidth <= step.clientWidth + 1,
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("replaces setter cages and generates a rated variant", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -9,19 +9,35 @@ describe("BoardViewport", () => {
|
||||
|
||||
it("zooms, fits and restores the persisted scale", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { unmount } = render(
|
||||
const { container, unmount } = render(
|
||||
<BoardViewport>
|
||||
<div>Board</div>
|
||||
</BoardViewport>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Board zoom level")).toHaveTextContent("100%");
|
||||
expect(container.querySelector(".board-viewport")).not.toHaveClass(
|
||||
"is-zoomed-in",
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Zoom board out" }));
|
||||
expect(screen.getByLabelText("Board zoom level")).toHaveTextContent("75%");
|
||||
expect(container.querySelector(".board-viewport__canvas")).toHaveStyle({
|
||||
width: "75%",
|
||||
maxWidth: "36rem",
|
||||
});
|
||||
expect(container.querySelector(".board-viewport")).not.toHaveClass(
|
||||
"is-zoomed-in",
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Zoom board in" }));
|
||||
await user.click(screen.getByRole("button", { name: "Zoom board in" }));
|
||||
expect(screen.getByLabelText("Board zoom level")).toHaveTextContent("125%");
|
||||
expect(container.querySelector(".board-viewport")).toHaveClass(
|
||||
"is-zoomed-in",
|
||||
);
|
||||
expect(localStorage.getItem(BOARD_SCALE_STORAGE_KEY)).toBe("1.25");
|
||||
unmount();
|
||||
|
||||
render(
|
||||
const restored = render(
|
||||
<BoardViewport>
|
||||
<div>Board</div>
|
||||
</BoardViewport>,
|
||||
@@ -29,6 +45,9 @@ describe("BoardViewport", () => {
|
||||
expect(screen.getByLabelText("Board zoom level")).toHaveTextContent("125%");
|
||||
await user.click(screen.getByRole("button", { name: "Fit board" }));
|
||||
expect(screen.getByLabelText("Board zoom level")).toHaveTextContent("100%");
|
||||
expect(restored.container.querySelector(".board-viewport")).not.toHaveClass(
|
||||
"is-zoomed-in",
|
||||
);
|
||||
});
|
||||
|
||||
it("offers explicit pan mode and board-scoped zoom shortcuts", async () => {
|
||||
|
||||
@@ -3,7 +3,42 @@ import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { NumberPad } from "../../src/components/NumberPad";
|
||||
|
||||
describe("NumberPad colour accessibility", () => {
|
||||
describe("NumberPad", () => {
|
||||
it("marks standard and large digit layouts and exposes a full-row erase key", () => {
|
||||
const { container, rerender } = render(
|
||||
<NumberPad
|
||||
size={9}
|
||||
mode="value"
|
||||
onMode={vi.fn()}
|
||||
onValue={vi.fn()}
|
||||
onErase={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("group", { name: "Digits" })).toHaveClass(
|
||||
"digit-pad",
|
||||
);
|
||||
expect(container.querySelector(".digit-pad")).not.toHaveClass(
|
||||
"digit-pad--wide",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Erase" })).toHaveClass(
|
||||
"erase-key",
|
||||
);
|
||||
|
||||
rerender(
|
||||
<NumberPad
|
||||
size={16}
|
||||
mode="value"
|
||||
onMode={vi.fn()}
|
||||
onValue={vi.fn()}
|
||||
onErase={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(".digit-pad")).toHaveClass(
|
||||
"digit-pad--wide",
|
||||
);
|
||||
});
|
||||
|
||||
it("names each colour by both hue and pattern", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onValue = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user