// @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; import type { Pf2eCreature } from "@initiative/domain"; import { creatureId } from "@initiative/domain"; import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import { Pf2eStatBlock } from "../pf2e-stat-block.js"; afterEach(cleanup); const PERCEPTION_SENSES_REGEX = /\+2.*Darkvision/; const SKILLS_REGEX = /Acrobatics \+5.*Stealth \+5/; const SAVE_CONDITIONAL_REGEX = /\+12.*\+1 status to all saves vs\. magic/; const SAVE_CONDITIONAL_ABSENT_REGEX = /status to all saves/; const HP_DETAILS_REGEX = /115.*regeneration 20/; const REGEN_REGEX = /regeneration/; const ATTACK_NAME_REGEX = /Dogslicer/; const ATTACK_DAMAGE_REGEX = /1d6 slashing/; const SPELLCASTING_ENTRY_REGEX = /Divine Innate Spells\./; const ABILITY_MID_NAME_REGEX = /Goblin Scuttle/; const ABILITY_MID_DESC_REGEX = /The goblin Steps\./; const CANTRIPS_REGEX = /Cantrips:/; const AC_REGEX = /16/; const GOBLIN_WARRIOR: Pf2eCreature = { system: "pf2e", id: creatureId("pathfinder-monster-core:goblin-warrior"), name: "Goblin Warrior", source: "pathfinder-monster-core", sourceDisplayName: "Monster Core", level: -1, traits: ["small", "goblin", "humanoid"], perception: 2, senses: "Darkvision", languages: "Common, Goblin", skills: "Acrobatics +5, Athletics +2, Nature +1, Stealth +5", abilityMods: { str: 0, dex: 3, con: 1, int: 0, wis: -1, cha: 1 }, ac: 16, saveFort: 5, saveRef: 7, saveWill: 3, hp: 6, speed: "25 feet", attacks: [ { name: "Dogslicer", activity: { number: 1, unit: "action" }, segments: [ { type: "text", value: "+7 (agile, backstabber, finesse), 1d6 slashing", }, ], }, ], abilitiesMid: [ { name: "Goblin Scuttle", activity: { number: 1, unit: "reaction" }, segments: [{ type: "text", value: "The goblin Steps." }], }, ], }; const NAUNET: Pf2eCreature = { system: "pf2e", id: creatureId("pathfinder-monster-core-2:naunet"), name: "Naunet", source: "pathfinder-monster-core-2", sourceDisplayName: "Monster Core 2", level: 7, traits: ["large", "monitor", "protean"], perception: 14, senses: "Darkvision", languages: "Chthonian, Empyrean, Protean", skills: "Acrobatics +14, Athletics +16, Intimidation +16, Stealth +14, Survival +12", abilityMods: { str: 5, dex: 3, con: 5, int: 0, wis: 3, cha: 3 }, ac: 24, saveFort: 18, saveRef: 14, saveWill: 12, saveConditional: "+1 status to all saves vs. magic", hp: 120, resistances: "Precision 5, Protean anatomy 10", speed: "25 feet, Fly 30 feet, Swim 25 feet (unfettered movement)", spellcasting: [ { name: "Divine Innate Spells", headerText: "DC 25, attack +17", daily: [ { uses: 4, each: true, spells: ["Unfettered Movement (Constant)"] }, ], atWill: ["Detect Magic"], }, ], }; const TROLL: Pf2eCreature = { system: "pf2e", id: creatureId("pathfinder-monster-core:forest-troll"), name: "Forest Troll", source: "pathfinder-monster-core", sourceDisplayName: "Monster Core", level: 5, traits: ["large", "giant", "troll"], perception: 11, senses: "Darkvision", languages: "Jotun", skills: "Athletics +12, Intimidation +12", abilityMods: { str: 5, dex: 2, con: 6, int: -2, wis: 0, cha: -2 }, ac: 20, saveFort: 17, saveRef: 11, saveWill: 7, hp: 115, hpDetails: "regeneration 20 (deactivated by acid or fire)", weaknesses: "Fire 10", speed: "30 feet", }; function renderStatBlock(creature: Pf2eCreature) { return render(); } describe("Pf2eStatBlock", () => { describe("header", () => { it("renders creature name and level", () => { renderStatBlock(GOBLIN_WARRIOR); expect( screen.getByRole("heading", { name: "Goblin Warrior" }), ).toBeInTheDocument(); expect(screen.getByText("Level -1")).toBeInTheDocument(); }); it("renders traits as tags", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Small")).toBeInTheDocument(); expect(screen.getByText("Goblin")).toBeInTheDocument(); expect(screen.getByText("Humanoid")).toBeInTheDocument(); }); it("renders source display name", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Monster Core")).toBeInTheDocument(); }); }); describe("perception and senses", () => { it("renders perception modifier and senses", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Perception")).toBeInTheDocument(); expect(screen.getByText(PERCEPTION_SENSES_REGEX)).toBeInTheDocument(); }); it("renders languages", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Languages")).toBeInTheDocument(); expect(screen.getByText("Common, Goblin")).toBeInTheDocument(); }); it("renders skills", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Skills")).toBeInTheDocument(); expect(screen.getByText(SKILLS_REGEX)).toBeInTheDocument(); }); }); describe("ability modifiers", () => { it("renders all six ability labels", () => { renderStatBlock(GOBLIN_WARRIOR); for (const label of ["Str", "Dex", "Con", "Int", "Wis", "Cha"]) { expect(screen.getByText(label)).toBeInTheDocument(); } }); it("renders positive and negative modifiers", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("+3")).toBeInTheDocument(); expect(screen.getByText("-1")).toBeInTheDocument(); }); }); describe("defenses", () => { it("renders AC and saves", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("AC")).toBeInTheDocument(); expect(screen.getByText(AC_REGEX)).toBeInTheDocument(); expect(screen.getByText("Fort")).toBeInTheDocument(); expect(screen.getByText("Ref")).toBeInTheDocument(); expect(screen.getByText("Will")).toBeInTheDocument(); }); it("renders HP", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("HP")).toBeInTheDocument(); expect(screen.getByText("6")).toBeInTheDocument(); }); it("renders saveConditional inline with saves", () => { renderStatBlock(NAUNET); expect(screen.getByText(SAVE_CONDITIONAL_REGEX)).toBeInTheDocument(); }); it("omits saveConditional when absent", () => { renderStatBlock(GOBLIN_WARRIOR); expect( screen.queryByText(SAVE_CONDITIONAL_ABSENT_REGEX), ).not.toBeInTheDocument(); }); it("renders hpDetails in parentheses after HP", () => { renderStatBlock(TROLL); expect(screen.getByText(HP_DETAILS_REGEX)).toBeInTheDocument(); }); it("omits hpDetails when absent", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.queryByText(REGEN_REGEX)).not.toBeInTheDocument(); }); it("renders resistances and weaknesses", () => { renderStatBlock(NAUNET); expect(screen.getByText("Resistances")).toBeInTheDocument(); expect( screen.getByText("Precision 5, Protean anatomy 10"), ).toBeInTheDocument(); }); }); describe("abilities", () => { it("renders mid (defensive) abilities", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText(ABILITY_MID_NAME_REGEX)).toBeInTheDocument(); expect(screen.getByText(ABILITY_MID_DESC_REGEX)).toBeInTheDocument(); }); }); describe("speed and attacks", () => { it("renders speed", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText("Speed")).toBeInTheDocument(); expect(screen.getByText("25 feet")).toBeInTheDocument(); }); it("renders attacks", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.getByText(ATTACK_NAME_REGEX)).toBeInTheDocument(); expect(screen.getByText(ATTACK_DAMAGE_REGEX)).toBeInTheDocument(); }); }); describe("spellcasting", () => { it("renders spellcasting entry with header", () => { renderStatBlock(NAUNET); expect(screen.getByText(SPELLCASTING_ENTRY_REGEX)).toBeInTheDocument(); expect(screen.getByText("DC 25, attack +17")).toBeInTheDocument(); }); it("renders ranked spells", () => { renderStatBlock(NAUNET); expect(screen.getByText("Rank 4:")).toBeInTheDocument(); expect( screen.getByText("Unfettered Movement (Constant)"), ).toBeInTheDocument(); }); it("renders cantrips", () => { renderStatBlock(NAUNET); expect(screen.getByText("Cantrips:")).toBeInTheDocument(); expect(screen.getByText("Detect Magic")).toBeInTheDocument(); }); it("omits spellcasting when absent", () => { renderStatBlock(GOBLIN_WARRIOR); expect(screen.queryByText(CANTRIPS_REGEX)).not.toBeInTheDocument(); }); }); });