mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 20:27:04 +01:00
These were unmasked, not caused, last session: `lint` is
`tsc --noEmit && eslint src`, and a broken tsc had been short-circuiting
the && so eslint had never run on this package.
The binding constraint is the 100% coverage gate, not the linter. Most
idiomatic fixes for no-non-null-assertion add a branch that no test can
reach, which trades a lint error for a coverage failure - and the
no-suppressions rule forbids escaping either way. So every fix here is
one that adds no branch:
- no-non-null-assertion 18 -> 2: a waitFor already proves result.current
is non-null, so capture it into a local instead of asserting on each
read. App.tsx's files[0]! was pure noise (the same position also
reported no-unnecessary-type-assertion). usePuzzleGameLoop's shift()!
became index-then-shift, since the while condition already indexes [0]
unasserted.
- non-nullable-type-assertion-style 9 -> 0: the rule-swap trap - eslint
asks for `!`, which the other rule forbids. Narrowed with instanceof.
- unbound-method 10 -> 1: keep the MockInstance vi.spyOn already returns
rather than detaching the method. This surfaced a latent bug: one test
calls restoreAllMocks() mid-test, so the beforeEach handle was stale.
- no-unnecessary-type-assertion 5 -> 1, require-await 1 -> 0.
Coverage held at 100%: 584/217/98/535 -> 585/217/98/536. The +1 statement
and +1 line are the index-then-shift split; branches unchanged at 217 is
the number that proves no fix added an untested path. 145 tests, tsc 0.
The 5 remaining are reported, not silenced. App.tsx's fileResult! needs
the phase state to carry the result ({ kind: "done"; result }) - three
smaller structures all failed the same way. The rest are documented in
the evidence file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
209 lines
5.9 KiB
TypeScript
209 lines
5.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
import type React from "react";
|
|
import { useGameLoop } from "./useGameLoop";
|
|
|
|
const makeRef = <T>(val: T): React.RefObject<T> => ({ current: val });
|
|
|
|
describe("useGameLoop", () => {
|
|
let rafCallbacks: FrameRequestCallback[];
|
|
|
|
beforeEach(() => {
|
|
rafCallbacks = [];
|
|
vi.stubGlobal(
|
|
"requestAnimationFrame",
|
|
vi.fn((cb: FrameRequestCallback) => {
|
|
rafCallbacks.push(cb);
|
|
return rafCallbacks.length;
|
|
}),
|
|
);
|
|
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const flushTick = (): void => {
|
|
const cb = rafCallbacks.pop();
|
|
if (cb) act(() => { cb(0); });
|
|
};
|
|
|
|
it("returns null when active=false and does not start RAF", () => {
|
|
const { result } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(null),
|
|
makeRef(400),
|
|
[],
|
|
false,
|
|
),
|
|
);
|
|
expect(result.current).toBeNull();
|
|
expect(requestAnimationFrame).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns null when canvasRef.current is null (active=true)", () => {
|
|
renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(null),
|
|
makeRef(400),
|
|
[new File(["x"], "a.txt")],
|
|
true,
|
|
),
|
|
);
|
|
expect(requestAnimationFrame).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("cancels RAF on unmount", () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 800;
|
|
canvas.height = 600;
|
|
const { unmount } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(canvas),
|
|
makeRef(400),
|
|
[],
|
|
true,
|
|
),
|
|
);
|
|
unmount();
|
|
expect(cancelAnimationFrame).toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves immediately with empty result when files is empty", async () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 800;
|
|
canvas.height = 600;
|
|
|
|
const { result } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(canvas),
|
|
makeRef(400),
|
|
[],
|
|
true,
|
|
),
|
|
);
|
|
|
|
flushTick(); // allDone=true on first frame → setResult
|
|
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
|
const settled = result.current;
|
|
if (settled === null) {
|
|
throw new Error('result never settled');
|
|
}
|
|
expect(settled.caught).toHaveLength(0);
|
|
expect(settled.missed).toHaveLength(0);
|
|
});
|
|
|
|
it("catches file and resolves result (caught branch + caught-draw branch)", async () => {
|
|
// Math.random()=0 → file x=48, speed=2.5
|
|
// basketXRef=48 → |fx-bx|=0<108, |fy-by|=35.5<48 → caught on tick 0
|
|
vi.spyOn(Math, "random").mockReturnValue(0);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 800;
|
|
canvas.height = 90; // basketY = 90-80 = 10
|
|
|
|
const { result } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(canvas),
|
|
makeRef(48),
|
|
[new File(["x"], "a.txt")],
|
|
true,
|
|
),
|
|
);
|
|
|
|
flushTick(); // tick 0: caught
|
|
flushTick(); // tick 1: draw caught + allDone=true → setResult
|
|
|
|
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
|
const settled = result.current;
|
|
if (settled === null) {
|
|
throw new Error('result never settled');
|
|
}
|
|
expect(settled.caught).toHaveLength(1);
|
|
expect(settled.missed).toHaveLength(0);
|
|
});
|
|
|
|
it("misses file (still-falling + missed branches)", async () => {
|
|
// Math.random()=0 → file x=48, speed=2.5; basketXRef=400
|
|
// |48-400|=352 > 108 → no collision; off-bottom at y>118, ~60 ticks
|
|
vi.spyOn(Math, "random").mockReturnValue(0);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 800;
|
|
canvas.height = 90;
|
|
|
|
const { result } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(canvas),
|
|
makeRef(400),
|
|
[new File(["x"], "a.txt")],
|
|
true,
|
|
),
|
|
);
|
|
|
|
for (let i = 0; i < 65; i++) {
|
|
if (result.current !== null) break;
|
|
flushTick();
|
|
}
|
|
|
|
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
|
const settled = result.current;
|
|
if (settled === null) {
|
|
throw new Error('result never settled');
|
|
}
|
|
expect(settled.caught).toHaveLength(0);
|
|
expect(settled.missed).toHaveLength(1);
|
|
});
|
|
|
|
it("does not start RAF when canvas.getContext returns null", () => {
|
|
// A plain fake canvas whose getContext returns null avoids prototype contamination
|
|
const fakeCanvas = {
|
|
width: 800,
|
|
height: 600,
|
|
clientWidth: 800,
|
|
clientHeight: 600,
|
|
getContext: () => null,
|
|
} as unknown as HTMLCanvasElement;
|
|
|
|
renderHook(() =>
|
|
useGameLoop(makeRef<HTMLCanvasElement | null>(fakeCanvas), makeRef(400), [], true),
|
|
);
|
|
|
|
expect(requestAnimationFrame).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles not-yet-spawned branch with two files (startFrames 0 and 50)", async () => {
|
|
// Both files: x=48, speed=2.5; basket at 48 → both caught
|
|
// File 1 has startFrame=50 → "frame < ff.startFrame" branch hit for ticks 1-49
|
|
vi.spyOn(Math, "random").mockReturnValue(0);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 800;
|
|
canvas.height = 90;
|
|
|
|
const { result } = renderHook(() =>
|
|
useGameLoop(
|
|
makeRef<HTMLCanvasElement | null>(canvas),
|
|
makeRef(48),
|
|
[new File(["a"], "a.txt"), new File(["b"], "b.txt")],
|
|
true,
|
|
),
|
|
);
|
|
|
|
for (let i = 0; i < 60; i++) {
|
|
if (result.current !== null) break;
|
|
flushTick();
|
|
}
|
|
|
|
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
|
const settled = result.current;
|
|
if (settled === null) {
|
|
throw new Error('result never settled');
|
|
}
|
|
expect(settled.caught).toHaveLength(2);
|
|
expect(settled.missed).toHaveLength(0);
|
|
});
|
|
});
|