import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { GeneratorWorkspace } from "../../src/components/GeneratorWorkspace"; import { classicRegions } from "../../src/domain"; import type { GeneratedVariantBatch, GeneratedVariantPuzzle, } from "../../src/solver"; describe("Sudoku generator workspace", () => { it("offers only reliable sizes and submits an explicit local recipe", async () => { const user = userEvent.setup(); const onGenerate = vi.fn(); render( , ); await user.selectOptions(screen.getByLabelText("Variant"), "anti-king"); const grid = screen.getByLabelText("Grid"); expect( within(grid) .getAllByRole("option") .map((option) => option.textContent), ).toEqual(["6 × 6", "9 × 9"]); expect( screen.queryByLabelText("Requested markings"), ).not.toBeInTheDocument(); await user.selectOptions(screen.getByLabelText("Variant"), "thermo"); expect(screen.getByLabelText("Requested markings")).toBeInTheDocument(); await user.selectOptions(screen.getByLabelText("Grid"), "4"); await user.selectOptions( screen.getByLabelText("Requested profile"), "easy", ); await user.clear(screen.getByLabelText("Seed")); await user.type(screen.getByLabelText("Seed"), "repeatable-demo"); await user.click(screen.getByRole("button", { name: "Generate Thermo" })); expect(onGenerate).toHaveBeenCalledWith({ variant: "thermo", size: 4, targetDifficulty: "easy", symmetry: "rotational", constraintCount: 8, seed: "repeatable-demo", }); }); it("exposes independent rating for the current puzzle", async () => { const user = userEvent.setup(); const onRate = vi.fn(); render( , ); await user.click( screen.getByRole("button", { name: "Rate current puzzle" }), ); expect(onRate).toHaveBeenCalledOnce(); }); it("submits a bounded evidence-backed practice target", async () => { const user = userEvent.setup(); const onGenerate = vi.fn(); render( , ); await user.selectOptions( screen.getByLabelText("Practice technique"), "x-wing", ); const attempts = screen.getByLabelText("Mining attempts"); await user.clear(attempts); await user.type(attempts, "7"); await user.clear(screen.getByLabelText("Seed")); await user.type(screen.getByLabelText("Seed"), "x-wing-training"); await user.click(screen.getByRole("button", { name: "Generate Classic" })); expect(onGenerate).toHaveBeenCalledWith( expect.objectContaining({ requiredTechnique: "x-wing", maxTechniqueAttempts: 7, seed: "x-wing-training", }), ); await user.selectOptions(screen.getByLabelText("Variant"), "killer"); expect( within(screen.getByLabelText("Practice technique")).getByRole("option", { name: "Killer Cage", }), ).toBeInTheDocument(); }); it("submits mixed families, density, minimality and a full profile as a batch", async () => { const user = userEvent.setup(); const onGenerateBatch = vi.fn(); render( , ); await user.selectOptions(screen.getByLabelText("Variant"), "thermo"); await user.click(screen.getByRole("checkbox", { name: "Kropki dots" })); await user.selectOptions( screen.getByLabelText("Given symmetry"), "horizontal", ); await user.selectOptions( screen.getByLabelText("Constraint density"), "dense", ); await user.click( screen.getByRole("checkbox", { name: "Prove minimal givens" }), ); await user.selectOptions( screen.getByLabelText("Practice technique"), "x-wing", ); await user.clear(screen.getByLabelText("Minimum occurrences")); await user.type(screen.getByLabelText("Minimum occurrences"), "2"); await user.type(screen.getByLabelText("Maximum occurrences"), "3"); await user.selectOptions( screen.getByLabelText("Forbidden technique"), "swordfish", ); await user.selectOptions( screen.getByLabelText("Hardest technique target"), "x-wing", ); await user.clear(screen.getByLabelText("Batch size")); await user.type(screen.getByLabelText("Batch size"), "3"); await user.selectOptions( screen.getByLabelText("Batch ranking"), "fewest-givens", ); await user.type(screen.getByLabelText("Seed"), "mixed-batch"); await user.click( screen.getByRole("button", { name: "Generate 3-puzzle batch" }), ); expect(onGenerateBatch).toHaveBeenCalledWith({ variant: "thermo", variants: ["thermo", "kropki"], size: 9, targetDifficulty: "medium", symmetry: "horizontal", constraintCount: 8, constraintDensity: "dense", minimalGivens: true, requiredTechnique: "x-wing", techniqueProfile: { forbidden: ["swordfish"], counts: [{ technique: "x-wing", min: 2, max: 3 }], hardestTechnique: "x-wing", }, maxTechniqueAttempts: 10, seed: "mixed-batch", batchSize: 3, ranking: "fewest-givens", }); }); it("renders ranked evidence and lets the caller open a candidate", async () => { const user = userEvent.setup(); const onSelectGenerated = vi.fn(); const solution = [1, 2, 3, 4, 3, 4, 1, 2, 4, 3, 2, 1, 2, 1, 4, 3]; const generation: GeneratedVariantPuzzle = { puzzle: { version: 1, size: 4, givens: solution, solution, regions: classicRegions(4), constraints: [], }, difficulty: { score: 12, level: "beginner", label: "Beginner", uniqueness: "unique", clueCount: 16, emptyCount: 0, logicalStatus: "solved", logicalSteps: 0, techniqueCounts: {}, exactNodes: 1, exactTruncated: false, summary: "fixture", }, variant: "classic", families: ["classic"], seed: "ranked:1", generatedConstraintCount: 0, generationAttempts: 1, constraintDensity: "balanced", minimality: { status: "not-requested", checksPerformed: 0, nodes: 0, removedClues: 0, criticalCells: [], unknownCells: [], limitReasons: [], symmetryPreserved: true, }, }; const batch: GeneratedVariantBatch = { entries: [generation], summaries: [ { rank: 1, seed: generation.seed, families: generation.families, clueCount: 16, constraintCount: 0, score: 12, level: "beginner", minimalityStatus: "not-requested", }, ], failures: [], requested: 1, completed: 1, truncated: false, ranking: "difficulty", baseSeed: "ranked", }; render( , ); expect(screen.getByText("1 of 1 generated")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Use" })); expect(onSelectGenerated).toHaveBeenCalledWith(generation); }); });