Add missing component and hook tests, raise coverage thresholds

13 new test files for untested components (color-palette, player-management,
stat-block, settings-modal, export/import dialogs, bulk-import-prompt,
source-fetch-prompt, player-character-section) and hooks (use-long-press,
use-swipe-to-dismiss, use-bulk-import, use-initiative-rolls). Expand
combatant-row tests with inline editing, HP popover, and condition picker.

Component coverage: 59% → 80% lines, 55% → 71% branches
Hook coverage: 72% → 83% lines, 55% → 66% branches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-29 23:55:21 +02:00
parent 08b5db81ad
commit b229a0dac7
15 changed files with 1802 additions and 4 deletions

View File

@@ -0,0 +1,273 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import type { Creature } from "@initiative/domain";
import { creatureId } from "@initiative/domain";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { StatBlock } from "../stat-block.js";
afterEach(cleanup);
const ARMOR_CLASS_REGEX = /Armor Class/;
const DEX_PLUS_4_REGEX = /Dex \+4/;
const CR_QUARTER_REGEX = /1\/4/;
const PROF_BONUS_2_REGEX = /Proficiency Bonus \+2/;
const NIMBLE_ESCAPE_REGEX = /Nimble Escape\./;
const SCIMITAR_REGEX = /Scimitar\./;
const DETECT_REGEX = /Detect\./;
const TAIL_ATTACK_REGEX = /Tail Attack\./;
const INNATE_SPELLCASTING_REGEX = /Innate Spellcasting\./;
const AT_WILL_REGEX = /At Will:/;
const DETECT_MAGIC_REGEX = /detect magic, suggestion/;
const DAILY_REGEX = /3\/day each:/;
const FIREBALL_REGEX = /fireball, wall of fire/;
const LONG_REST_REGEX = /1\/long rest:/;
const WISH_REGEX = /wish/;
const GOBLIN: Creature = {
id: creatureId("srd:goblin"),
name: "Goblin",
source: "MM",
sourceDisplayName: "Monster Manual",
size: "Small",
type: "humanoid",
alignment: "neutral evil",
ac: 15,
acSource: "leather armor, shield",
hp: { average: 7, formula: "2d6" },
speed: "30 ft.",
abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 },
cr: "1/4",
initiativeProficiency: 0,
proficiencyBonus: 2,
passive: 9,
savingThrows: "Dex +4",
skills: "Stealth +6",
senses: "darkvision 60 ft., passive Perception 9",
languages: "Common, Goblin",
traits: [{ name: "Nimble Escape", text: "Disengage or Hide as bonus." }],
actions: [{ name: "Scimitar", text: "Melee: +4 to hit, 5 slashing." }],
bonusActions: [{ name: "Nimble", text: "Disengage or Hide." }],
reactions: [{ name: "Redirect", text: "Redirect attack to ally." }],
};
const DRAGON: Creature = {
id: creatureId("srd:dragon"),
name: "Ancient Red Dragon",
source: "MM",
sourceDisplayName: "Monster Manual",
size: "Gargantuan",
type: "dragon",
alignment: "chaotic evil",
ac: 22,
hp: { average: 546, formula: "28d20 + 252" },
speed: "40 ft., climb 40 ft., fly 80 ft.",
abilities: { str: 30, dex: 10, con: 29, int: 18, wis: 15, cha: 23 },
cr: "24",
initiativeProficiency: 0,
proficiencyBonus: 7,
passive: 26,
resist: "fire",
immune: "fire",
vulnerable: "cold",
conditionImmune: "frightened",
legendaryActions: {
preamble: "The dragon can take 3 legendary actions.",
entries: [
{ name: "Detect", text: "Wisdom (Perception) check." },
{ name: "Tail Attack", text: "Tail attack." },
],
},
spellcasting: [
{
name: "Innate Spellcasting",
headerText: "The dragon's spellcasting ability is Charisma.",
atWill: ["detect magic", "suggestion"],
daily: [{ uses: 3, each: true, spells: ["fireball", "wall of fire"] }],
restLong: [{ uses: 1, each: false, spells: ["wish"] }],
},
],
};
function renderStatBlock(creature: Creature) {
return render(<StatBlock creature={creature} />);
}
describe("StatBlock", () => {
describe("header", () => {
it("renders creature name", () => {
renderStatBlock(GOBLIN);
expect(
screen.getByRole("heading", { name: "Goblin" }),
).toBeInTheDocument();
});
it("renders size, type, alignment", () => {
renderStatBlock(GOBLIN);
expect(
screen.getByText("Small humanoid, neutral evil"),
).toBeInTheDocument();
});
it("renders source display name", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
});
});
describe("stats bar", () => {
it("renders AC with source", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText(ARMOR_CLASS_REGEX)).toBeInTheDocument();
expect(screen.getByText("15")).toBeInTheDocument();
expect(screen.getByText("(leather armor, shield)")).toBeInTheDocument();
});
it("renders AC without source when acSource is undefined", () => {
renderStatBlock(DRAGON);
expect(screen.getByText("22")).toBeInTheDocument();
});
it("renders HP average and formula", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("7")).toBeInTheDocument();
expect(screen.getByText("(2d6)")).toBeInTheDocument();
});
it("renders speed", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("30 ft.")).toBeInTheDocument();
});
});
describe("ability scores", () => {
it("renders all 6 ability labels", () => {
renderStatBlock(GOBLIN);
for (const label of ["STR", "DEX", "CON", "INT", "WIS", "CHA"]) {
expect(screen.getByText(label)).toBeInTheDocument();
}
});
it("renders ability scores with modifier notation", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("(+2)")).toBeInTheDocument();
});
});
describe("properties", () => {
it("renders saving throws when present", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("Saving Throws")).toBeInTheDocument();
expect(screen.getByText(DEX_PLUS_4_REGEX)).toBeInTheDocument();
});
it("renders skills when present", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("Skills")).toBeInTheDocument();
});
it("renders damage resistances, immunities, vulnerabilities", () => {
renderStatBlock(DRAGON);
expect(screen.getByText("Damage Resistances")).toBeInTheDocument();
expect(screen.getByText("Damage Immunities")).toBeInTheDocument();
expect(screen.getByText("Damage Vulnerabilities")).toBeInTheDocument();
expect(screen.getByText("Condition Immunities")).toBeInTheDocument();
});
it("omits properties when undefined", () => {
renderStatBlock(GOBLIN);
expect(screen.queryByText("Damage Resistances")).not.toBeInTheDocument();
expect(screen.queryByText("Damage Immunities")).not.toBeInTheDocument();
});
it("renders CR and proficiency bonus", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText("Challenge")).toBeInTheDocument();
expect(screen.getByText(CR_QUARTER_REGEX)).toBeInTheDocument();
expect(screen.getByText(PROF_BONUS_2_REGEX)).toBeInTheDocument();
});
});
describe("traits", () => {
it("renders trait entries", () => {
renderStatBlock(GOBLIN);
expect(screen.getByText(NIMBLE_ESCAPE_REGEX)).toBeInTheDocument();
});
});
describe("actions / bonus actions / reactions", () => {
it("renders actions heading and entries", () => {
renderStatBlock(GOBLIN);
expect(
screen.getByRole("heading", { name: "Actions" }),
).toBeInTheDocument();
expect(screen.getByText(SCIMITAR_REGEX)).toBeInTheDocument();
});
it("renders bonus actions heading and entries", () => {
renderStatBlock(GOBLIN);
expect(
screen.getByRole("heading", { name: "Bonus Actions" }),
).toBeInTheDocument();
});
it("renders reactions heading and entries", () => {
renderStatBlock(GOBLIN);
expect(
screen.getByRole("heading", { name: "Reactions" }),
).toBeInTheDocument();
});
});
describe("legendary actions", () => {
it("renders legendary actions with preamble", () => {
renderStatBlock(DRAGON);
expect(
screen.getByRole("heading", { name: "Legendary Actions" }),
).toBeInTheDocument();
expect(
screen.getByText("The dragon can take 3 legendary actions."),
).toBeInTheDocument();
expect(screen.getByText(DETECT_REGEX)).toBeInTheDocument();
expect(screen.getByText(TAIL_ATTACK_REGEX)).toBeInTheDocument();
});
it("omits legendary actions when undefined", () => {
renderStatBlock(GOBLIN);
expect(
screen.queryByRole("heading", { name: "Legendary Actions" }),
).not.toBeInTheDocument();
});
});
describe("spellcasting", () => {
it("renders spellcasting block with header", () => {
renderStatBlock(DRAGON);
expect(screen.getByText(INNATE_SPELLCASTING_REGEX)).toBeInTheDocument();
});
it("renders at-will spells", () => {
renderStatBlock(DRAGON);
expect(screen.getByText(AT_WILL_REGEX)).toBeInTheDocument();
expect(screen.getByText(DETECT_MAGIC_REGEX)).toBeInTheDocument();
});
it("renders daily spells", () => {
renderStatBlock(DRAGON);
expect(screen.getByText(DAILY_REGEX)).toBeInTheDocument();
expect(screen.getByText(FIREBALL_REGEX)).toBeInTheDocument();
});
it("renders long rest spells", () => {
renderStatBlock(DRAGON);
expect(screen.getByText(LONG_REST_REGEX)).toBeInTheDocument();
expect(screen.getByText(WISH_REGEX)).toBeInTheDocument();
});
it("omits spellcasting when undefined", () => {
renderStatBlock(GOBLIN);
expect(screen.queryByText(AT_WILL_REGEX)).not.toBeInTheDocument();
});
});
});