mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 19:27:05 +01:00
Frontend (React 19 + Vite 6 + TypeScript strict): - DropZone, ModeSelect, GameCanvas, PuzzleCanvas, ScoreScreen, PuzzleResult - File-drop game with AABB collision; download (JSZip) and upload (NestJS) modes - Puzzle mode: NxN image slice via OffscreenCanvas; Union-Find spatial clustering guarantees 100% catch rate is always achievable regardless of piece speeds - ESLint typescript-eslint strict-type-checked (zero errors) - 145 Vitest tests; 100% coverage on statements/branches/functions/lines Backend (NestJS 11): - POST /files/upload (multer disk storage) and GET /health Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ8QTmreFcaqrsvVb38Grd
223 lines
8.0 KiB
TypeScript
223 lines
8.0 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
||
import { render, screen, fireEvent } from "@testing-library/react";
|
||
import { DropZone } from "./DropZone";
|
||
|
||
const makeFile = (name: string, type = "text/plain"): File =>
|
||
new File(["x"], name, { type });
|
||
|
||
describe("DropZone", () => {
|
||
it("renders title and subtitle", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
expect(screen.getByText("Bucket Catch")).toBeInTheDocument();
|
||
expect(
|
||
screen.getByText(/Drop your files here/),
|
||
).toBeInTheDocument();
|
||
});
|
||
|
||
it("shows initial grid equation", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
|
||
});
|
||
|
||
it("dragOver adds dragging class; dragLeave removes it", () => {
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||
);
|
||
const zone = container.firstChild as HTMLElement;
|
||
expect(zone.className).not.toContain("dragging");
|
||
fireEvent.dragOver(zone);
|
||
expect(zone.className).toContain("dragging");
|
||
fireEvent.dragLeave(zone);
|
||
expect(zone.className).not.toContain("dragging");
|
||
});
|
||
|
||
it("drop with files calls onFiles and shows count (plural)", () => {
|
||
const onFiles = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||
);
|
||
const zone = container.firstChild as HTMLElement;
|
||
const f1 = makeFile("a.txt");
|
||
const f2 = makeFile("b.txt");
|
||
fireEvent.drop(zone, { dataTransfer: { files: [f1, f2] } });
|
||
expect(onFiles).toHaveBeenCalledWith([f1, f2]);
|
||
expect(screen.getByText("2 files ready")).toBeInTheDocument();
|
||
});
|
||
|
||
it("drop with files shows singular count for 1 file", () => {
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||
);
|
||
const zone = container.firstChild as HTMLElement;
|
||
fireEvent.drop(zone, { dataTransfer: { files: [makeFile("a.txt")] } });
|
||
expect(screen.getByText("1 file ready")).toBeInTheDocument();
|
||
});
|
||
|
||
it("drop with empty files does not call onFiles", () => {
|
||
const onFiles = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||
);
|
||
const zone = container.firstChild as HTMLElement;
|
||
fireEvent.drop(zone, { dataTransfer: { files: [] } });
|
||
expect(onFiles).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("file input change with files calls onFiles", () => {
|
||
const onFiles = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||
);
|
||
const fileInput = container.querySelector(
|
||
"input[type='file'][multiple]",
|
||
) as HTMLInputElement;
|
||
const file = makeFile("c.txt");
|
||
Object.defineProperty(fileInput, "files", {
|
||
value: [file],
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(fileInput);
|
||
expect(onFiles).toHaveBeenCalledWith([file]);
|
||
});
|
||
|
||
it("file input change with empty files does not call onFiles", () => {
|
||
const onFiles = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||
);
|
||
const fileInput = container.querySelector(
|
||
"input[type='file'][multiple]",
|
||
) as HTMLInputElement;
|
||
Object.defineProperty(fileInput, "files", {
|
||
value: [],
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(fileInput);
|
||
expect(onFiles).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("file input change with null files (nullish-coalesce branch) does not call onFiles", () => {
|
||
const onFiles = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||
);
|
||
const fileInput = container.querySelector(
|
||
"input[type='file'][multiple]",
|
||
) as HTMLInputElement;
|
||
Object.defineProperty(fileInput, "files", {
|
||
value: null,
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(fileInput);
|
||
expect(onFiles).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("puzzle input change with file calls onPuzzle with current gridSize", () => {
|
||
const onPuzzle = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||
);
|
||
const puzzleInput = container.querySelector(
|
||
"input[accept='image/*']",
|
||
) as HTMLInputElement;
|
||
const imgFile = makeFile("photo.png", "image/png");
|
||
Object.defineProperty(puzzleInput, "files", {
|
||
value: [imgFile],
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(puzzleInput);
|
||
expect(onPuzzle).toHaveBeenCalledWith(imgFile, 4); // default gridSize = 4
|
||
});
|
||
|
||
it("puzzle input change with null files does not call onPuzzle (optional-chain null branch)", () => {
|
||
const onPuzzle = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||
);
|
||
const puzzleInput = container.querySelector(
|
||
"input[accept='image/*']",
|
||
) as HTMLInputElement;
|
||
Object.defineProperty(puzzleInput, "files", {
|
||
value: null,
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(puzzleInput);
|
||
expect(onPuzzle).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("puzzle input change with empty files does not call onPuzzle (no first element)", () => {
|
||
const onPuzzle = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||
);
|
||
const puzzleInput = container.querySelector(
|
||
"input[accept='image/*']",
|
||
) as HTMLInputElement;
|
||
Object.defineProperty(puzzleInput, "files", {
|
||
value: [],
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(puzzleInput);
|
||
expect(onPuzzle).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("Play Puzzle Mode button triggers click on hidden puzzle input", () => {
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||
);
|
||
const puzzleInput = container.querySelector(
|
||
"input[accept='image/*']",
|
||
) as HTMLInputElement;
|
||
const clickSpy = vi.spyOn(puzzleInput, "click").mockImplementation(
|
||
() => undefined,
|
||
);
|
||
fireEvent.click(screen.getByText(/Play Puzzle Mode/));
|
||
expect(clickSpy).toHaveBeenCalled();
|
||
});
|
||
|
||
it("clicking preset updates gridSize display and equation", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
fireEvent.click(screen.getByText("2×2"));
|
||
expect(screen.getByText("2×2 = 4 pieces")).toBeInTheDocument();
|
||
});
|
||
|
||
it("custom grid input with valid value updates gridSize", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
const input = screen.getByLabelText("Custom grid size");
|
||
fireEvent.change(input, { target: { value: "7" } });
|
||
expect(screen.getByText("7×7 = 49 pieces")).toBeInTheDocument();
|
||
});
|
||
|
||
it("custom grid input with NaN value does not update gridSize", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
const input = screen.getByLabelText("Custom grid size");
|
||
fireEvent.change(input, { target: { value: "xyz" } });
|
||
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
|
||
});
|
||
|
||
it("custom grid input with value below 2 does not update gridSize", () => {
|
||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||
const input = screen.getByLabelText("Custom grid size");
|
||
fireEvent.change(input, { target: { value: "1" } });
|
||
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
|
||
});
|
||
|
||
it("onPuzzle uses updated gridSize after preset click", () => {
|
||
const onPuzzle = vi.fn();
|
||
const { container } = render(
|
||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||
);
|
||
fireEvent.click(screen.getByText("3×3")); // change gridSize to 3
|
||
const puzzleInput = container.querySelector(
|
||
"input[accept='image/*']",
|
||
) as HTMLInputElement;
|
||
const imgFile = makeFile("img.png", "image/png");
|
||
Object.defineProperty(puzzleInput, "files", {
|
||
value: [imgFile],
|
||
configurable: true,
|
||
});
|
||
fireEvent.change(puzzleInput);
|
||
expect(onPuzzle).toHaveBeenCalledWith(imgFile, 3);
|
||
});
|
||
});
|