bucket_catch: share the must() DOM helper from src/test/

The eslint commit added must() to DropZone.test.tsx, which pushed that
file from exactly 250 lines to 256 - so it removed lint errors while
silently adding an over-cap file. Moving the helper to
src/test/domQueries.ts drops it to 229 and takes the session's over-cap
count to its intended 41 -> 39.

src/test/ is the right home: coverage excludes that path, which is why
the fixtures already live there. PuzzleResult.test.tsx now uses the same
helper instead of its own inline instanceof guard.

145 tests, coverage 100% with totals unchanged at 579/217/98/530, eslint
still at the 4 known remaining errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-17 11:57:53 +02:00
parent 031bff898e
commit b866561c27
3 changed files with 35 additions and 40 deletions

View File

@ -1,31 +1,16 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { DropZone } from "./DropZone";
import { must } from "../test/domQueries";
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} />);
expect(screen.getByText("Bucket Catch")).toBeInTheDocument();
expect(
screen.getByText(/Drop your files here/),
).toBeInTheDocument();
expect(screen.getByText(/Drop your files here/)).toBeInTheDocument();
});
it("shows initial grid equation", () => {
@ -37,10 +22,7 @@ describe("DropZone", () => {
const { container } = render(
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
);
const zone = must(
container.firstChild as Element | null,
"the drop zone",
);
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");
@ -53,10 +35,7 @@ describe("DropZone", () => {
const { container } = render(
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
);
const zone = must(
container.firstChild as Element | null,
"the drop zone",
);
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] } });
@ -68,10 +47,7 @@ describe("DropZone", () => {
const { container } = render(
<DropZone onFiles={() => undefined} onPuzzle={() => undefined} />,
);
const zone = must(
container.firstChild as Element | null,
"the drop zone",
);
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();
});
@ -81,10 +57,7 @@ describe("DropZone", () => {
const { container } = render(
<DropZone onFiles={onFiles} onPuzzle={() => undefined} />,
);
const zone = must(
container.firstChild as Element | null,
"the drop zone",
);
const zone = must(container.firstChild as Element | null, "the drop zone");
fireEvent.drop(zone, { dataTransfer: { files: [] } });
expect(onFiles).not.toHaveBeenCalled();
});
@ -201,9 +174,9 @@ describe("DropZone", () => {
container.querySelector("input[accept='image/*']"),
"input[accept='image/*']",
);
const clickSpy = vi.spyOn(puzzleInput, "click").mockImplementation(
() => undefined,
);
const clickSpy = vi
.spyOn(puzzleInput, "click")
.mockImplementation(() => undefined);
fireEvent.click(screen.getByText(/Play Puzzle Mode/));
expect(clickSpy).toHaveBeenCalled();
});

View File

@ -1,6 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { PuzzleResult } from "./PuzzleResult";
import { must } from "../test/domQueries";
import type { FallingPuzzleItem, PuzzlePiece } from "../types";
const makePiece = (row: number, col: number): PuzzlePiece => ({
@ -129,10 +130,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"]');
if (!(gridEl instanceof HTMLElement)) {
throw new Error("expected to find the puzzle grid element");
}
const gridEl = must(
container.querySelector('[style*="repeat"]'),
"the puzzle grid element",
);
const holeDivs = Array.from(gridEl.children).filter(
(c) => c.tagName === "DIV",
);

View File

@ -0,0 +1,21 @@
/**
* DOM query helpers shared by the component tests.
*
* Lives under src/test/ because the coverage config excludes that path — a
* helper placed beside a component would be counted as production code and
* break the 100% gate.
*/
/**
* 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.
*/
export const must = (el: Element | null, what: string): HTMLElement => {
if (!(el instanceof HTMLElement)) {
throw new Error(`expected to find ${what}`);
}
return el;
};