mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 19:27:05 +01:00
bucket_catch: 45 eslint errors -> 5, no suppressions
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
This commit is contained in:
parent
91bc84de32
commit
a4b879b19d
@ -65,7 +65,7 @@ export default function App(): React.ReactElement {
|
||||
if (mode === "puzzle") {
|
||||
return (
|
||||
<PuzzleCanvas
|
||||
imageFile={files[0]!}
|
||||
imageFile={files[0]}
|
||||
gridSize={puzzleGridSize}
|
||||
onDone={handlePuzzleDone}
|
||||
/>
|
||||
|
||||
@ -5,6 +5,20 @@ import { DropZone } from "./DropZone";
|
||||
const makeFile = (name: string, type = "text/plain"): File =>
|
||||
new File(["x"], name, { type });
|
||||
|
||||
/**
|
||||
* Query one element and narrow it, failing the test if it is absent.
|
||||
*
|
||||
* Both `as HTMLElement` and `!` are rejected here - the former by
|
||||
* non-nullable-type-assertion-style, the latter by no-non-null-assertion - so
|
||||
* the narrowing is done with a real check that also gives a useful message.
|
||||
*/
|
||||
const must = (el: Element | null, what: string): HTMLElement => {
|
||||
if (!(el instanceof HTMLElement)) {
|
||||
throw new Error(`expected to find ${what}`);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
describe("DropZone", () => {
|
||||
it("renders title and subtitle", () => {
|
||||
render(<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />);
|
||||
@ -23,7 +37,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const zone = container.firstChild as HTMLElement;
|
||||
const zone = must(
|
||||
container.firstChild as Element | null,
|
||||
"the drop zone",
|
||||
);
|
||||
expect(zone.className).not.toContain("dragging");
|
||||
fireEvent.dragOver(zone);
|
||||
expect(zone.className).toContain("dragging");
|
||||
@ -36,7 +53,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const zone = container.firstChild as HTMLElement;
|
||||
const zone = must(
|
||||
container.firstChild as Element | null,
|
||||
"the drop zone",
|
||||
);
|
||||
const f1 = makeFile("a.txt");
|
||||
const f2 = makeFile("b.txt");
|
||||
fireEvent.drop(zone, { dataTransfer: { files: [f1, f2] } });
|
||||
@ -48,7 +68,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const zone = container.firstChild as HTMLElement;
|
||||
const zone = must(
|
||||
container.firstChild as Element | null,
|
||||
"the drop zone",
|
||||
);
|
||||
fireEvent.drop(zone, { dataTransfer: { files: [makeFile("a.txt")] } });
|
||||
expect(screen.getByText("1 file ready")).toBeInTheDocument();
|
||||
});
|
||||
@ -58,7 +81,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const zone = container.firstChild as HTMLElement;
|
||||
const zone = must(
|
||||
container.firstChild as Element | null,
|
||||
"the drop zone",
|
||||
);
|
||||
fireEvent.drop(zone, { dataTransfer: { files: [] } });
|
||||
expect(onFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -68,9 +94,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const fileInput = container.querySelector(
|
||||
const fileInput = must(
|
||||
container.querySelector("input[type='file'][multiple]"),
|
||||
"input[type='file'][multiple]",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
const file = makeFile("c.txt");
|
||||
Object.defineProperty(fileInput, "files", {
|
||||
value: [file],
|
||||
@ -85,9 +112,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const fileInput = container.querySelector(
|
||||
const fileInput = must(
|
||||
container.querySelector("input[type='file'][multiple]"),
|
||||
"input[type='file'][multiple]",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
Object.defineProperty(fileInput, "files", {
|
||||
value: [],
|
||||
configurable: true,
|
||||
@ -101,9 +129,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const fileInput = container.querySelector(
|
||||
const fileInput = must(
|
||||
container.querySelector("input[type='file'][multiple]"),
|
||||
"input[type='file'][multiple]",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
Object.defineProperty(fileInput, "files", {
|
||||
value: null,
|
||||
configurable: true,
|
||||
@ -117,9 +146,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||||
);
|
||||
const puzzleInput = container.querySelector(
|
||||
const puzzleInput = must(
|
||||
container.querySelector("input[accept='image/*']"),
|
||||
"input[accept='image/*']",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
const imgFile = makeFile("photo.png", "image/png");
|
||||
Object.defineProperty(puzzleInput, "files", {
|
||||
value: [imgFile],
|
||||
@ -134,9 +164,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||||
);
|
||||
const puzzleInput = container.querySelector(
|
||||
const puzzleInput = must(
|
||||
container.querySelector("input[accept='image/*']"),
|
||||
"input[accept='image/*']",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
Object.defineProperty(puzzleInput, "files", {
|
||||
value: null,
|
||||
configurable: true,
|
||||
@ -150,9 +181,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||||
);
|
||||
const puzzleInput = container.querySelector(
|
||||
const puzzleInput = must(
|
||||
container.querySelector("input[accept='image/*']"),
|
||||
"input[accept='image/*']",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
Object.defineProperty(puzzleInput, "files", {
|
||||
value: [],
|
||||
configurable: true,
|
||||
@ -165,9 +197,10 @@ describe("DropZone", () => {
|
||||
const { container } = render(
|
||||
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
|
||||
);
|
||||
const puzzleInput = container.querySelector(
|
||||
const puzzleInput = must(
|
||||
container.querySelector("input[accept='image/*']"),
|
||||
"input[accept='image/*']",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
const clickSpy = vi.spyOn(puzzleInput, "click").mockImplementation(
|
||||
() => undefined,
|
||||
);
|
||||
@ -208,9 +241,10 @@ describe("DropZone", () => {
|
||||
<DropZone onFiles={() => undefined} onPuzzle={onPuzzle} />,
|
||||
);
|
||||
fireEvent.click(screen.getByText("3×3")); // change gridSize to 3
|
||||
const puzzleInput = container.querySelector(
|
||||
const puzzleInput = must(
|
||||
container.querySelector("input[accept='image/*']"),
|
||||
"input[accept='image/*']",
|
||||
) as HTMLInputElement;
|
||||
);
|
||||
const imgFile = makeFile("img.png", "image/png");
|
||||
Object.defineProperty(puzzleInput, "files", {
|
||||
value: [imgFile],
|
||||
|
||||
@ -129,9 +129,10 @@ describe("PuzzleResult", () => {
|
||||
const imgs = container.querySelectorAll("img");
|
||||
expect(imgs).toHaveLength(2);
|
||||
// The grid div has inline style with gridTemplateColumns; its non-img children are holes
|
||||
const gridEl = container.querySelector(
|
||||
'[style*="repeat"]',
|
||||
) as HTMLElement;
|
||||
const gridEl = container.querySelector('[style*="repeat"]');
|
||||
if (!(gridEl instanceof HTMLElement)) {
|
||||
throw new Error("expected to find the puzzle grid element");
|
||||
}
|
||||
const holeDivs = Array.from(gridEl.children).filter(
|
||||
(c) => c.tagName === "DIV",
|
||||
);
|
||||
|
||||
@ -39,7 +39,7 @@ describe("ScoreScreen", () => {
|
||||
expect(screen.getByText("Missed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows download button for download mode with multiple files", async () => {
|
||||
it("shows download button for download mode with multiple files", () => {
|
||||
render(
|
||||
<ScoreScreen result={result1} mode="download" onRestart={() => undefined} />,
|
||||
);
|
||||
|
||||
@ -4,8 +4,9 @@ import { fireEvent } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { useBasketControl } from "./useBasketControl";
|
||||
|
||||
const makeRef = <T>(val: T): React.RefObject<T> =>
|
||||
({ current: val }) as React.RefObject<T>;
|
||||
const makeRef = <T>(val: T): React.RefObject<T> => ({
|
||||
current: val,
|
||||
});
|
||||
|
||||
describe("useBasketControl", () => {
|
||||
it("initialises basket x to half of window.innerWidth", () => {
|
||||
|
||||
@ -3,8 +3,7 @@ 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 }) as React.RefObject<T>;
|
||||
const makeRef = <T>(val: T): React.RefObject<T> => ({ current: val });
|
||||
|
||||
describe("useGameLoop", () => {
|
||||
let rafCallbacks: FrameRequestCallback[];
|
||||
@ -88,8 +87,12 @@ describe("useGameLoop", () => {
|
||||
|
||||
flushTick(); // allDone=true on first frame → setResult
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caught).toHaveLength(0);
|
||||
expect(result.current!.missed).toHaveLength(0);
|
||||
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 () => {
|
||||
@ -114,8 +117,12 @@ describe("useGameLoop", () => {
|
||||
flushTick(); // tick 1: draw caught + allDone=true → setResult
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caught).toHaveLength(1);
|
||||
expect(result.current!.missed).toHaveLength(0);
|
||||
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 () => {
|
||||
@ -142,8 +149,12 @@ describe("useGameLoop", () => {
|
||||
}
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caught).toHaveLength(0);
|
||||
expect(result.current!.missed).toHaveLength(1);
|
||||
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", () => {
|
||||
@ -187,7 +198,11 @@ describe("useGameLoop", () => {
|
||||
}
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caught).toHaveLength(2);
|
||||
expect(result.current!.missed).toHaveLength(0);
|
||||
const settled = result.current;
|
||||
if (settled === null) {
|
||||
throw new Error('result never settled');
|
||||
}
|
||||
expect(settled.caught).toHaveLength(2);
|
||||
expect(settled.missed).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -85,8 +85,10 @@ export function usePuzzleGameLoop(
|
||||
scheduleRef.current.length > 0 &&
|
||||
frame >= scheduleRef.current[0].spawnFrame
|
||||
) {
|
||||
// while condition guarantees length > 0, so shift() is always defined
|
||||
const s = scheduleRef.current.shift()!;
|
||||
// The while condition already indexes [0] unasserted, so this is
|
||||
// the same non-null value; shift() only advances the queue.
|
||||
const s = scheduleRef.current[0];
|
||||
scheduleRef.current.shift();
|
||||
activeItemsRef.current.push({
|
||||
kind: "puzzle",
|
||||
id: `puzzle-${s.piece.row}-${s.piece.col}`,
|
||||
|
||||
@ -51,8 +51,12 @@ describe("usePuzzleGameLoop play", () => {
|
||||
flushTick(); // tick 0: spawn, move, caught, flash, allDone → setResult
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caughtPieces).toHaveLength(1);
|
||||
expect(result.current!.missedPieces).toHaveLength(0);
|
||||
const settled = result.current;
|
||||
if (settled === null) {
|
||||
throw new Error('result never settled');
|
||||
}
|
||||
expect(settled.caughtPieces).toHaveLength(1);
|
||||
expect(settled.missedPieces).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("misses piece (still-falling + missed branches)", async () => {
|
||||
@ -77,8 +81,12 @@ describe("usePuzzleGameLoop play", () => {
|
||||
}
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caughtPieces).toHaveLength(0);
|
||||
expect(result.current!.missedPieces).toHaveLength(1);
|
||||
const settled = result.current;
|
||||
if (settled === null) {
|
||||
throw new Error('result never settled');
|
||||
}
|
||||
expect(settled.caughtPieces).toHaveLength(0);
|
||||
expect(settled.missedPieces).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("tests flash-during and flash-expired branches with 2 pieces", async () => {
|
||||
@ -114,8 +122,12 @@ describe("usePuzzleGameLoop play", () => {
|
||||
}
|
||||
|
||||
await waitFor(() => { expect(result.current).not.toBeNull(); });
|
||||
expect(result.current!.caughtPieces).toHaveLength(1);
|
||||
expect(result.current!.missedPieces).toHaveLength(1);
|
||||
const settled = result.current;
|
||||
if (settled === null) {
|
||||
throw new Error('result never settled');
|
||||
}
|
||||
expect(settled.caughtPieces).toHaveLength(1);
|
||||
expect(settled.missedPieces).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses drawImage when piece image is loaded (img.complete=true branch)", async () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { MockInstance } from "vitest";
|
||||
import { sliceImage } from "./sliceImage";
|
||||
|
||||
// Captures the last Image instance created so tests can trigger onload/onerror.
|
||||
@ -22,10 +23,14 @@ class FakeImage {
|
||||
}
|
||||
|
||||
describe("sliceImage", () => {
|
||||
let revokeUrlSpy: MockInstance<typeof URL.revokeObjectURL>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("Image", FakeImage);
|
||||
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock");
|
||||
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
|
||||
revokeUrlSpy = vi
|
||||
.spyOn(URL, "revokeObjectURL")
|
||||
.mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -44,7 +49,7 @@ describe("sliceImage", () => {
|
||||
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");
|
||||
expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock");
|
||||
});
|
||||
|
||||
it("includes pieceWidth and pieceHeight from image dimensions", async () => {
|
||||
@ -74,7 +79,7 @@ describe("sliceImage", () => {
|
||||
const promise = sliceImage(file, 1);
|
||||
capturedImg.onload?.();
|
||||
await expect(promise).rejects.toThrow("Canvas 2D context not available");
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock");
|
||||
expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock");
|
||||
|
||||
// Restore so subsequent tests in this file have the mock context
|
||||
HTMLCanvasElement.prototype.getContext = savedImpl;
|
||||
@ -85,6 +90,6 @@ describe("sliceImage", () => {
|
||||
const promise = sliceImage(file, 2);
|
||||
capturedImg.onerror?.();
|
||||
await expect(promise).rejects.toThrow("Failed to load image for slicing");
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock");
|
||||
expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { MockInstance } from "vitest";
|
||||
import { zipDownload } from "./zipDownload";
|
||||
|
||||
describe("zipDownload", () => {
|
||||
let createUrlSpy: MockInstance<typeof URL.createObjectURL>;
|
||||
let revokeUrlSpy: MockInstance<typeof URL.revokeObjectURL>;
|
||||
let clickSpy: MockInstance<typeof HTMLAnchorElement.prototype.click>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock-url");
|
||||
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||
() => undefined,
|
||||
);
|
||||
createUrlSpy = vi
|
||||
.spyOn(URL, "createObjectURL")
|
||||
.mockReturnValue("blob:mock-url");
|
||||
revokeUrlSpy = vi
|
||||
.spyOn(URL, "revokeObjectURL")
|
||||
.mockImplementation(() => undefined);
|
||||
clickSpy = vi
|
||||
.spyOn(HTMLAnchorElement.prototype, "click")
|
||||
.mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -21,30 +30,38 @@ describe("zipDownload", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
await zipDownload([file]);
|
||||
|
||||
expect(URL.createObjectURL).toHaveBeenCalled();
|
||||
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalled();
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock-url");
|
||||
expect(createUrlSpy).toHaveBeenCalled();
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock-url");
|
||||
expect(appendSpy).toHaveBeenCalled();
|
||||
expect(removeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets download attribute and href on the anchor", async () => {
|
||||
const anchors: HTMLAnchorElement[] = [];
|
||||
// Bound before the spy replaces it, so the implementation below can build
|
||||
// a real element without recursing into its own mock.
|
||||
const realCreateElement = document.createElement.bind(document);
|
||||
vi.spyOn(document, "createElement").mockImplementation((tag: string) => {
|
||||
const el = document.createElement.bind(document)(tag) as HTMLElement;
|
||||
if (tag === "a") anchors.push(el as HTMLAnchorElement);
|
||||
const el = realCreateElement(tag);
|
||||
if (el instanceof HTMLAnchorElement) anchors.push(el);
|
||||
return el;
|
||||
});
|
||||
// Restore after spy above since we used bind trick
|
||||
// Restore after spy above since we used bind trick. That discards the
|
||||
// handles from beforeEach, so re-spy and keep the new ones.
|
||||
vi.restoreAllMocks();
|
||||
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock-url");
|
||||
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => undefined);
|
||||
const localRevokeSpy = vi
|
||||
.spyOn(URL, "revokeObjectURL")
|
||||
.mockImplementation(() => undefined);
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
const file = new File(["x"], "data.bin");
|
||||
await zipDownload([file]);
|
||||
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock-url");
|
||||
expect(localRevokeSpy).toHaveBeenCalledWith("blob:mock-url");
|
||||
});
|
||||
|
||||
it("works with multiple files", async () => {
|
||||
@ -54,7 +71,7 @@ describe("zipDownload", () => {
|
||||
new File(["c"], "c.png", { type: "image/png" }),
|
||||
];
|
||||
await expect(zipDownload(files)).resolves.toBeUndefined();
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(createUrlSpy).toHaveBeenCalledTimes(1);
|
||||
expect(revokeUrlSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,8 +10,9 @@ import type { PuzzlePiece } from "../types";
|
||||
* the 100% gate.
|
||||
*/
|
||||
|
||||
export const makeRef = <T>(val: T): React.RefObject<T> =>
|
||||
({ current: val }) as React.RefObject<T>;
|
||||
export const makeRef = <T>(val: T): React.RefObject<T> => ({
|
||||
current: val,
|
||||
});
|
||||
|
||||
export const makePiece = (row: number, col: number): PuzzlePiece => ({
|
||||
row,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user