182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
GuidedHint,
|
|
type GuidedHintProps,
|
|
} from "../../src/components/GuidedHint";
|
|
import {
|
|
deriveGuidedHintCellSets,
|
|
guidedHintEffectItems,
|
|
guidedHintFocusSummary,
|
|
guidedHintOverlay,
|
|
guidedHintStepIsVisible,
|
|
nextGuidedHintStage,
|
|
} from "../../src/components/guidedHint";
|
|
import type { LogicalStep } from "../../src/solver";
|
|
|
|
const step: LogicalStep = {
|
|
technique: "naked-pair",
|
|
focusCells: [0, 1, 0],
|
|
placements: [{ cell: 9, value: 4 }],
|
|
eliminations: [{ cell: 10, values: [2, 3] }],
|
|
explanation: "The private pair reasoning is now visible.",
|
|
};
|
|
|
|
function props(overrides: Partial<GuidedHintProps> = {}): GuidedHintProps {
|
|
return {
|
|
size: 9,
|
|
step,
|
|
stage: "focus",
|
|
autoMaintainPeerNotes: false,
|
|
onRequestHint: vi.fn(),
|
|
onRevealNext: vi.fn(),
|
|
onApply: vi.fn(),
|
|
onDismiss: vi.fn(),
|
|
onFillLegalCandidates: vi.fn(),
|
|
onRemoveInvalidNotes: vi.fn(),
|
|
onAutoMaintainPeerNotesChange: vi.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("guided hint presentation helpers", () => {
|
|
it("derives unique focus and effect cells without revealing effects early", () => {
|
|
expect(deriveGuidedHintCellSets(step)).toEqual({
|
|
focusCells: [0, 1],
|
|
placementCells: [9],
|
|
eliminationCells: [10],
|
|
affectedCells: [9, 10],
|
|
});
|
|
expect(guidedHintOverlay(step, "reasoning")).toEqual({
|
|
focusCells: [0, 1],
|
|
placementCells: [],
|
|
eliminationCells: [],
|
|
});
|
|
expect(guidedHintOverlay(step, "preview")).toEqual({
|
|
focusCells: [0, 1],
|
|
placementCells: [9],
|
|
eliminationCells: [10],
|
|
});
|
|
});
|
|
|
|
it("describes focus locations, effects and the finite reveal sequence", () => {
|
|
expect(guidedHintFocusSummary(step, 9)).toBe("Look across row 1.");
|
|
expect(guidedHintFocusSummary({ ...step, focusCells: [0, 9] }, 9)).toBe(
|
|
"Look down column 1.",
|
|
);
|
|
expect(guidedHintFocusSummary({ ...step, focusCells: [0, 10] }, 9)).toBe(
|
|
"Look within box 1.",
|
|
);
|
|
expect(guidedHintEffectItems(step, 9)).toEqual([
|
|
"Place 4 in r2c1.",
|
|
"Remove 2 and 3 from r2c2.",
|
|
]);
|
|
expect(nextGuidedHintStage("focus")).toBe("technique");
|
|
expect(nextGuidedHintStage("technique")).toBe("reasoning");
|
|
expect(nextGuidedHintStage("reasoning")).toBe("preview");
|
|
expect(nextGuidedHintStage("preview")).toBeUndefined();
|
|
});
|
|
|
|
it("rejects a hint when fog hides a premise, placement or elimination", () => {
|
|
expect(guidedHintStepIsVisible(step, new Set())).toBe(true);
|
|
expect(guidedHintStepIsVisible(step, new Set([0]))).toBe(false);
|
|
expect(guidedHintStepIsVisible(step, new Set([9]))).toBe(false);
|
|
expect(guidedHintStepIsVisible(step, new Set([10]))).toBe(false);
|
|
expect(guidedHintStepIsVisible(step, new Set([80]))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("guided hint", () => {
|
|
it("keeps unrevealed answer content out of the document", () => {
|
|
const base = props();
|
|
const { rerender } = render(<GuidedHint {...base} />);
|
|
|
|
expect(screen.getByText("Look across row 1.")).toBeInTheDocument();
|
|
expect(screen.queryByText("Naked Pair")).not.toBeInTheDocument();
|
|
expect(screen.queryByText(step.explanation)).not.toBeInTheDocument();
|
|
expect(screen.queryByText("Place 4 in r2c1.")).not.toBeInTheDocument();
|
|
|
|
rerender(<GuidedHint {...base} stage="technique" />);
|
|
expect(screen.getByText("Naked Pair")).toBeInTheDocument();
|
|
expect(screen.queryByText(step.explanation)).not.toBeInTheDocument();
|
|
expect(screen.queryByText("Place 4 in r2c1.")).not.toBeInTheDocument();
|
|
|
|
rerender(<GuidedHint {...base} stage="reasoning" />);
|
|
expect(screen.getByText(step.explanation)).toBeInTheDocument();
|
|
expect(screen.queryByText("Place 4 in r2c1.")).not.toBeInTheDocument();
|
|
|
|
rerender(<GuidedHint {...base} stage="preview" />);
|
|
expect(screen.getByText("Place 4 in r2c1.")).toBeInTheDocument();
|
|
expect(screen.getByText("Remove 2 and 3 from r2c2.")).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(/will start a complete legal centre-candidate grid/u),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("button", { name: "Apply this step" }),
|
|
).toBeEnabled();
|
|
});
|
|
|
|
it("delegates reveal, apply, replacement and dismissal to its parent", async () => {
|
|
const user = userEvent.setup();
|
|
const callbacks = {
|
|
onRevealNext: vi.fn(),
|
|
onApply: vi.fn(),
|
|
onRequestHint: vi.fn(),
|
|
onDismiss: vi.fn(),
|
|
};
|
|
const { rerender } = render(
|
|
<GuidedHint {...props(callbacks)} stage="focus" />,
|
|
);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Reveal technique" }));
|
|
expect(callbacks.onRevealNext).toHaveBeenCalledOnce();
|
|
|
|
await user.click(screen.getByRole("button", { name: "New hint" }));
|
|
await user.click(screen.getByRole("button", { name: "Dismiss" }));
|
|
expect(callbacks.onRequestHint).toHaveBeenCalledOnce();
|
|
expect(callbacks.onDismiss).toHaveBeenCalledOnce();
|
|
|
|
rerender(<GuidedHint {...props(callbacks)} stage="preview" />);
|
|
await user.click(screen.getByRole("button", { name: "Apply this step" }));
|
|
expect(callbacks.onApply).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("offers candidate maintenance without requiring an open hint", async () => {
|
|
const user = userEvent.setup();
|
|
const onRequestHint = vi.fn();
|
|
const onFillLegalCandidates = vi.fn();
|
|
const onRemoveInvalidNotes = vi.fn();
|
|
const onAutoMaintainPeerNotesChange = vi.fn();
|
|
render(
|
|
<GuidedHint
|
|
{...props({
|
|
step: undefined,
|
|
onRequestHint,
|
|
onFillLegalCandidates,
|
|
onRemoveInvalidNotes,
|
|
onAutoMaintainPeerNotesChange,
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Get a guided hint" }));
|
|
await user.click(
|
|
screen.getByRole("button", { name: "Fill legal candidates" }),
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: "Remove invalid notes" }),
|
|
);
|
|
await user.click(
|
|
screen.getByRole("checkbox", {
|
|
name: "Automatically remove peer notes after placing a digit",
|
|
}),
|
|
);
|
|
|
|
expect(onRequestHint).toHaveBeenCalledOnce();
|
|
expect(onFillLegalCandidates).toHaveBeenCalledOnce();
|
|
expect(onRemoveInvalidNotes).toHaveBeenCalledOnce();
|
|
expect(onAutoMaintainPeerNotesChange).toHaveBeenCalledWith(true);
|
|
});
|
|
});
|