testsAndMisc/kcd2_dice_solver/src/components/componentResult.test.tsx
Krzysztof kuhy Rudnicki 776e04db89 Split the kcd2_dice_solver test files under the cap
Nine new modules from four over-cap test files. Every kcd2 test file is
now under 250 lines; only four source files remain.

App.test.tsx ended up with no describe blocks and vitest failed with
"No test suite found" - a split that empties a file has to delete it.
The shared `response`/`ordinary` fixtures went to src/test/fixtures.ts,
which vite.config.ts excludes from coverage; beside the source they
would have counted as production code and broken the 100% gate.

288 tests passing before and after, now across 24 files instead of 15,
coverage still 100% on all four metrics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 20:03:33 +02:00

146 lines
5.3 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.

/** Result panel and inventory IO component tests. */
/** Badge picker and result panel component tests. */
/** Row, list and picker component tests. */
/**
* Component tests for the three ways of entering an inventory: clicking a die,
* scrolling the wheel over its counter, and fuzzy-searching by name.
*/
import { render, screen, } from "@testing-library/react";
import { ordinary, response } from "../test/fixtures.ts";
import { describe, expect, it, } from "vitest";
import type { Die } from "../data/dice.ts";
import type { SolveResponse } from "../core/solve.ts";
import { ResultPanel, } from "./ResultPanel.tsx";
/**
* Apply the updater a component emitted, to assert on the resulting count.
*
* @param mock - The mocked onChange.
* @param previous - The count it should be applied to.
* @param call - Which call to read, counting from the end by default.
* @returns The count the component asked for.
*/
describe("ResultPanel", () => {
it("prompts for dice before the first solve", () => {
render(<ResultPanel result={null} error={null} solving={false} />);
expect(screen.getByText(/Add at least six dice/)).toBeInTheDocument();
});
it("says it is working while a solve is in flight", () => {
render(<ResultPanel result={null} error={null} solving />);
expect(screen.getByText("Solving…")).toBeInTheDocument();
});
it("shows an error instead of a stale answer", () => {
render(<ResultPanel result={null} error="Need at least 6 dice" solving={false} />);
expect(screen.getByText("Need at least 6 dice")).toBeInTheDocument();
});
it("shows the loadout and its numbers", () => {
render(<ResultPanel result={response} error={null} solving={false} />);
expect(screen.getByText("6×")).toBeInTheDocument();
expect(screen.getByText("Ordinary die")).toBeInTheDocument();
expect(screen.getByText("399")).toBeInTheDocument();
expect(screen.getByText("3.09%")).toBeInTheDocument();
expect(screen.getByText("900")).toBeInTheDocument();
});
it("says when the answer is proven rather than merely good", () => {
render(<ResultPanel result={response} error={null} solving={false} />);
expect(screen.getByText(/Provably optimal/)).toBeInTheDocument();
});
it("admits when the answer is only a local search result", () => {
render(
<ResultPanel result={{ ...response, optimal: false }} error={null} solving={false} />,
);
expect(screen.getByText(/not proven/)).toBeInTheDocument();
});
it("lists runners-up when there are any", () => {
const withAlternatives: SolveResponse = {
...response,
alternatives: [
{ dice: new Array<Die>(6).fill(ordinary), evaluation: response.evaluation },
],
};
render(<ResultPanel result={withAlternatives} error={null} solving={false} />);
expect(screen.getByText("Runners-up")).toBeInTheDocument();
});
it("asks for badges when none are owned", () => {
render(<ResultPanel result={response} error={null} solving={false} />);
expect(screen.getByText(/Tick the badges you own/)).toBeInTheDocument();
});
it("shows the pick, its value and its reasoning for each tier", () => {
const withBadges: SolveResponse = {
...response,
badges: [
{
tier: "gold",
ranked: [
{
badge: {
id: "gold_emperors",
name: "Gold Emperor's badge",
description: "",
tier: "gold",
effect: { kind: "scoring", rules: { emperorTriple: true } },
},
pointsPerGame: 1234,
reason: "Changes the scoring table.",
dice: new Array<Die>(6).fill(ordinary),
},
{
badge: {
id: "gold_warlord",
name: "Gold Warlord badge",
description: "",
tier: "gold",
effect: { kind: "multiplier", factor: 2, uses: 1 },
},
pointsPerGame: 567,
reason: "Doubles a good turn.",
dice: null,
},
{
badge: {
id: "gold_defence",
name: "Gold Defence badge",
description: "",
tier: "gold",
effect: { kind: "defence" },
},
pointsPerGame: null,
reason: "Situational.",
dice: null,
},
],
},
],
};
render(<ResultPanel result={withBadges} error={null} solving={false} />);
expect(screen.getByText("Gold Emperor's badge")).toBeInTheDocument();
expect(screen.getByText(/~1,234 pts\/game/)).toBeInTheDocument();
expect(screen.getByText("Changes the scoring table.")).toBeInTheDocument();
expect(screen.getByText(/With this badge, bring/)).toBeInTheDocument();
// The runners-up list renders both a valued badge and a situational one.
expect(screen.getByText(/Gold Warlord badge — ~567 pts\/game/)).toBeInTheDocument();
expect(screen.getByText(/Gold Defence badge — situational/)).toBeInTheDocument();
});
it("dims the panel while a fresh solve is running", () => {
const { container } = render(
<ResultPanel result={response} error={null} solving />,
);
expect(container.querySelector(".result")).toHaveClass("stale");
});
});