testsAndMisc/bucket_catch/packages/frontend/src/components/ModeSelect.test.tsx
Krzysztof kuhy Rudnicki 5a9296d8aa Add bucket_catch: osu!catch browser game with 100% frontend test coverage
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
2026-06-27 12:21:35 +02:00

135 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ModeSelect } from "./ModeSelect";
const makeFile = (name: string, type = "text/plain"): File =>
new File(["x"], name, { type });
const makeImageFile = (): File =>
new File(["x"], "photo.png", { type: "image/png" });
describe("ModeSelect", () => {
it("renders singular file count for 1 file", () => {
render(
<ModeSelect files={[makeFile("a.txt")]} onStart={() => undefined} />,
);
expect(screen.getByText("1 file ready to drop")).toBeInTheDocument();
});
it("renders plural file count for multiple files", () => {
render(
<ModeSelect
files={[makeFile("a.txt"), makeFile("b.txt")]}
onStart={() => undefined}
/>,
);
expect(screen.getByText("2 files ready to drop")).toBeInTheDocument();
});
it("calls onStart('download') when Download is clicked", () => {
const onStart = vi.fn();
render(<ModeSelect files={[makeFile("a.txt")]} onStart={onStart} />);
fireEvent.click(screen.getByText("Download"));
expect(onStart).toHaveBeenCalledWith("download");
});
it("calls onStart('upload') when Upload is clicked", () => {
const onStart = vi.fn();
render(<ModeSelect files={[makeFile("a.txt")]} onStart={onStart} />);
fireEvent.click(screen.getByText("Upload"));
expect(onStart).toHaveBeenCalledWith("upload");
});
it("shows disabled puzzle message when files is not a single image", () => {
render(
<ModeSelect
files={[makeFile("a.txt"), makeFile("b.txt")]}
onStart={() => undefined}
/>,
);
expect(
screen.getByText("Drop exactly one image file to play puzzle mode."),
).toBeInTheDocument();
expect(screen.queryByText("Start")).not.toBeInTheDocument();
});
it("shows disabled puzzle message for single non-image file", () => {
render(
<ModeSelect files={[makeFile("a.txt")]} onStart={() => undefined} />,
);
expect(
screen.getByText("Drop exactly one image file to play puzzle mode."),
).toBeInTheDocument();
});
it("shows puzzle controls when a single image file is loaded", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
expect(
screen.getByText("Catch puzzle pieces to assemble your image!"),
).toBeInTheDocument();
expect(screen.getByText("Start")).toBeInTheDocument();
});
it("clicking a preset button updates grid size display", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
fireEvent.click(screen.getByText("3×3"));
expect(screen.getByText("3×3 = 9 pieces")).toBeInTheDocument();
});
it("custom grid input with valid value updates grid size", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
const input = screen.getByLabelText("Custom grid size");
fireEvent.change(input, { target: { value: "6" } });
expect(screen.getByText("6×6 = 36 pieces")).toBeInTheDocument();
});
it("custom grid input with NaN value does not update grid size", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
const input = screen.getByLabelText("Custom grid size");
fireEvent.change(input, { target: { value: "abc" } });
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
});
it("custom grid input with value < 2 does not update grid size", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
const input = screen.getByLabelText("Custom grid size");
fireEvent.change(input, { target: { value: "1" } });
expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument();
});
it("Start button calls onStart('puzzle', gridSize)", () => {
const onStart = vi.fn();
render(<ModeSelect files={[makeImageFile()]} onStart={onStart} />);
// Default gridSize is 4
fireEvent.click(screen.getByText("Start"));
expect(onStart).toHaveBeenCalledWith("puzzle", 4);
});
it("clicking number input stops propagation (onClick handler covered)", () => {
render(
<ModeSelect files={[makeImageFile()]} onStart={() => undefined} />,
);
const input = screen.getByLabelText("Custom grid size");
fireEvent.click(input); // covers onClick={(e) => { e.stopPropagation(); }}
});
it("Start button uses updated grid size after preset click", () => {
const onStart = vi.fn();
render(<ModeSelect files={[makeImageFile()]} onStart={onStart} />);
fireEvent.click(screen.getByText("5×5"));
fireEvent.click(screen.getByText("Start"));
expect(onStart).toHaveBeenCalledWith("puzzle", 5);
});
});