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 0df5e4ddad
commit 05d8c429d1
4 changed files with 72 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;
};

View File

@ -0,0 +1,37 @@
{
"intent": "Move the must() DOM narrowing helper out of DropZone.test.tsx, which the eslint pass had pushed 6 lines over the 250-line cap, into the shared coverage-excluded test directory where both component tests can use it.",
"scope": [
"New bucket_catch/packages/frontend/src/test/domQueries.ts",
"DropZone.test.tsx 256 -> 243 lines; PuzzleResult.test.tsx switched to the shared helper",
"Non-goal: no behaviour change, no assertion weakened"
],
"changes": [
"Extracted must() into src/test/domQueries.ts. That directory is excluded by the coverage config, which is why fixtures already live there - a helper beside a component would be counted as production code and break the 100% gate.",
"DropZone.test.tsx imports it instead of defining it, dropping the file back under the cap.",
"PuzzleResult.test.tsx replaced its inline instanceof guard with the same helper, so the two component tests narrow DOM queries identically rather than each rolling their own."
],
"verification": [
{
"command": "bash ~/utils/scripts/check_file_length.sh --all",
"result": "pass",
"evidence": "40 over-cap files before this change, 39 after; DropZone.test.tsx no longer appears. It had been 250 exactly and the must() helper pushed it to 256, so the eslint commit had silently added an over-cap file while removing others - this restores the intended 41 -> 39 for the session."
},
{
"command": "npm run coverage",
"result": "pass",
"evidence": "100% on all four measures with totals unchanged at 579 statements / 217 branches / 98 functions / 530 lines, and 145 tests in 15 files. Identical totals are the check that moving the helper into a coverage-excluded directory neither removed code from test reach nor added any."
},
{
"command": "npx tsc --noEmit && npx eslint src",
"result": "pass",
"evidence": "tsc exit 0; eslint still reports exactly the 4 known remaining errors, so the move introduced none."
}
],
"risks": [
"src/test/domQueries.ts is not coverage-measured, so a bug in must() would not be caught by the coverage gate. It is four lines with a single instanceof check, and every caller exercises it on the happy path in tests that assert on the returned element."
],
"rollback": [
"git revert the commit. must() returns to DropZone.test.tsx and that file goes back over the cap at 256 lines; PuzzleResult.test.tsx regains its inline guard.",
"After rollback, confirm npm run coverage still reports 100% with 579/217/98/530 and 145 passing tests."
]
}