testsAndMisc/bucket_catch/packages/frontend/src/lib/sliceImage.test.ts
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

91 lines
3.0 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, beforeEach, afterEach } from "vitest";
import { sliceImage } from "./sliceImage";
// Captures the last Image instance created so tests can trigger onload/onerror.
let capturedImg: {
onload: (() => void) | null;
onerror: (() => void) | null;
src: string;
width: number;
height: number;
};
class FakeImage {
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
src = "";
width = 200;
height = 100;
constructor() {
capturedImg = this;
}
}
describe("sliceImage", () => {
beforeEach(() => {
vi.stubGlobal("Image", FakeImage);
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock");
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("resolves with pieces when image loads", async () => {
const file = new File([""], "img.png", { type: "image/png" });
const promise = sliceImage(file, 2);
// Trigger onload synchronously — sliceImage has already assigned it.
capturedImg.onload?.();
const pieces = await promise;
expect(pieces).toHaveLength(4); // 2×2 grid
expect(pieces[0]).toMatchObject({ row: 0, col: 0, gridSize: 2 });
expect(pieces[3]).toMatchObject({ row: 1, col: 1, gridSize: 2 });
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock");
});
it("includes pieceWidth and pieceHeight from image dimensions", async () => {
const file = new File([""], "img.png");
const promise = sliceImage(file, 2);
capturedImg.onload?.();
const pieces = await promise;
// FakeImage.width=200, height=100, gridSize=2 → pieceWidth=100, pieceHeight=50
expect(pieces[0].pieceWidth).toBe(100);
expect(pieces[0].pieceHeight).toBe(50);
});
it("resolves with 1 piece for 1×1 grid", async () => {
const file = new File([""], "img.png");
const promise = sliceImage(file, 1);
capturedImg.onload?.();
const pieces = await promise;
expect(pieces).toHaveLength(1);
});
it("rejects when offscreen canvas getContext returns null", async () => {
// Save and override the prototype mock to return null for this test only
const savedImpl = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
const file = new File([""], "img.png");
const promise = sliceImage(file, 1);
capturedImg.onload?.();
await expect(promise).rejects.toThrow("Canvas 2D context not available");
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock");
// Restore so subsequent tests in this file have the mock context
HTMLCanvasElement.prototype.getContext = savedImpl;
});
it("rejects when image fails to load", async () => {
const file = new File([""], "broken.jpg");
const promise = sliceImage(file, 2);
capturedImg.onerror?.();
await expect(promise).rejects.toThrow("Failed to load image for slicing");
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock");
});
});