import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { ImportExportDialog } from "../../src/components/ImportExportDialog"; import { createEmptyPuzzle } from "../../src/domain"; import type { PuzzleDefinition } from "../../src/domain/types"; import { fromDomainPuzzle, type PreservedSudokuDocumentExtras, } from "../../src/formats"; import type { PortableAidMemoire } from "../../src/state/aidMemoire"; import { createSession } from "../../src/state/session"; const TEST_AID_MEMOIRE: PortableAidMemoire = { version: 1, enabled: true, columns: 1, cells: [ { label: "A", value: 2, cornerMarks: [], centerMarks: [], color: 3, }, ], }; function localScl(overrides: Record = {}) { const cells = Array.from({ length: 4 }, () => Array.from({ length: 4 }, () => ({})), ); cells[0]![0] = { value: 1 }; return JSON.stringify({ id: "synthetic-local-test", cells, metadata: { title: "Synthetic local puzzle" }, ...overrides, }); } function renderDialog( options: { readonly puzzle?: PuzzleDefinition; readonly aidMemoire?: PortableAidMemoire; readonly preservedExtras?: PreservedSudokuDocumentExtras; } = {}, ) { const puzzle = options.puzzle ?? createEmptyPuzzle(4); const onClose = vi.fn(); const onImport = vi.fn(); render( , ); return { onClose, onImport }; } describe("ImportExportDialog interoperability", () => { it("reports a detected compatible format before importing it", async () => { const user = userEvent.setup(); const { onClose, onImport } = renderDialog(); fireEvent.change(screen.getByPlaceholderText(/Paste 81 characters/u), { target: { value: localScl() }, }); await user.click( screen.getByRole("button", { name: "Check compatibility" }), ); expect( await screen.findByText( /SudokuPad\/CTC JSON: 4×4, 1 givens and 0 constraints/u, ), ).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Import locally" })); expect(onImport).toHaveBeenCalledWith( expect.objectContaining({ size: 4, givens: expect.any(Array) }), expect.any(Object), expect.objectContaining({ source: { format: "sudokupad", id: "synthetic-local-test" }, }), ); expect(onClose).toHaveBeenCalledOnce(); }); it("previews safe visuals and never fetches short IDs", async () => { const user = userEvent.setup(); const fetchSpy = vi.fn(); vi.stubGlobal("fetch", fetchSpy); renderDialog(); const input = screen.getByPlaceholderText(/Paste 81 characters/u); fireEvent.change(input, { target: { value: localScl({ overlays: [ { center: [0.5, 0.5], width: 1, height: 1, text: "visual only", }, ], }), }, }); await user.click( screen.getByRole("button", { name: "Check compatibility" }), ); expect( await screen.findByRole("region", { name: "Import mapping preview" }), ).toBeInTheDocument(); expect(screen.getByText("Preserved visuals")).toBeInTheDocument(); expect(screen.getByText("Text overlay: 1")).toBeInTheDocument(); fireEvent.change(input, { target: { value: localScl({ overlays: [{ text: "missing center" }] }) }, }); await user.click( screen.getByRole("button", { name: "Check compatibility" }), ); expect(await screen.findByText(/center.*point/u)).toBeInTheDocument(); fireEvent.change(input, { target: { value: "https://sudokupad.app/serverOnly42" }, }); await user.click( screen.getByRole("button", { name: "Check compatibility" }), ); expect( await screen.findByText(/server-hosted SudokuPad puzzle ID/u), ).toBeInTheDocument(); expect(fetchSpy).not.toHaveBeenCalled(); vi.unstubAllGlobals(); }); it("downloads the current puzzle as a self-contained SVG", async () => { const user = userEvent.setup(); let downloaded: Blob | undefined; vi.mocked(URL.createObjectURL).mockImplementation((value) => { downloaded = value as Blob; return "blob:visual-export-test"; }); vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}); renderDialog(); await user.click(screen.getByRole("button", { name: "Download SVG" })); expect( await screen.findByText("SVG rendered and downloaded locally."), ).toBeInTheDocument(); expect(downloaded?.type).toContain("image/svg+xml"); expect(await downloaded?.text()).toContain(" { const user = userEvent.setup(); const puzzle = createEmptyPuzzle(4); const { onImport } = renderDialog({ puzzle, aidMemoire: TEST_AID_MEMOIRE, }); let downloaded: Blob | undefined; vi.mocked(URL.createObjectURL).mockImplementation((value) => { downloaded = value as Blob; return "blob:project-export-test"; }); vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}); await user.click( screen.getByRole("button", { name: "Download project JSON" }), ); expect( await screen.findByText("Project JSON downloaded locally."), ).toBeInTheDocument(); expect(JSON.parse((await downloaded?.text()) ?? "{}").aidMemoire).toEqual( TEST_AID_MEMOIRE, ); const importedDocument = { ...fromDomainPuzzle(puzzle), aidMemoire: TEST_AID_MEMOIRE, }; fireEvent.change(screen.getByPlaceholderText(/Paste 81 characters/u), { target: { value: JSON.stringify(importedDocument) }, }); await user.click(screen.getByRole("button", { name: "Import locally" })); expect(onImport).toHaveBeenCalledWith( expect.objectContaining({ size: 4 }), expect.objectContaining({ aidMemoire: TEST_AID_MEMOIRE }), { source: { format: "sudoku-tools" } }, ); }); it("includes preserved source extras in SudokuPad exports", async () => { const user = userEvent.setup(); let downloaded: Blob | undefined; vi.mocked(URL.createObjectURL).mockImplementation((value) => { downloaded = value as Blob; return "blob:scl-export-test"; }); vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}); renderDialog({ preservedExtras: { source: { format: "sudokupad", id: "kept-source" }, metadata: { edition: "nightly" }, visuals: [ { type: "text", layer: "overlay", position: { kind: "cell", cell: 0 }, text: "kept visual", style: { fill: "#123456" }, }, ], }, }); await user.click( screen.getByRole("button", { name: "Download SudokuPad JSON" }), ); const exported = JSON.parse((await downloaded?.text()) ?? "{}") as { metadata?: Record; overlays?: unknown[]; }; expect(exported.metadata?.edition).toBe("nightly"); expect(exported.overlays).toEqual( expect.arrayContaining([ expect.objectContaining({ text: "kept visual" }), ]), ); }); it("reports an f-puzzles export incompatibility without throwing", async () => { const user = userEvent.setup(); renderDialog({ puzzle: { ...createEmptyPuzzle(4), constraints: [{ type: "xv", a: 0, b: 1, total: 5, negated: true }], }, }); await user.click( screen.getByRole("button", { name: "Download f-puzzles JSON" }), ); expect( await screen.findByText(/cannot preserve an individually false clue/u), ).toBeInTheDocument(); }); });