Files
pf1e-simulator/tests/test_combatant_loader.py
T
ctan 82148cea1e test(loaders): add unit tests for combatant feat resolution and feature/spell handling
Tests for _resolve_feat (known/unknown/Toughness/Weapon Focus),
_resolve_features (filtering, empty lists), load_combatant with
features+spells integration, and backward-compat aliases.

Gate: 401 tests, ruff clean, basedpyright clean.
2026-08-18 15:11:28 +02:00

183 lines
6.5 KiB
Python

"""Unit tests for the combatant loader feat resolution and feature/spell handling."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
from pf1e_simulator.abilities import (
DODGE,
IRON_WILL,
POWER_ATTACK,
toughness,
weapon_focus,
)
from pf1e_simulator.loaders.combatant import (
CombatantLoadError,
MonsterLoadError,
_resolve_feat,
_resolve_features,
load_combatant,
load_monster,
)
# ── _resolve_feat ─────────────────────────────────────────────────────────────
class TestResolveFeat:
def test_known_feat_returns_constant(self) -> None:
assert _resolve_feat("Power Attack", 1) is POWER_ATTACK
def test_known_feat_case_insensitive(self) -> None:
assert _resolve_feat("power attack", 1) is POWER_ATTACK
assert _resolve_feat("IRON WILL", 1) is IRON_WILL
def test_toughness_uses_level(self) -> None:
feat = _resolve_feat("Toughness", 5)
assert feat.name == "Toughness"
assert feat.category == "feat"
assert feat.effects == toughness(5).effects
def test_toughness_different_levels(self) -> None:
low = _resolve_feat("Toughness", 1)
high = _resolve_feat("Toughness", 10)
assert low.effects != high.effects
def test_weapon_focus_extracts_weapon(self) -> None:
feat = _resolve_feat("Weapon Focus (Longsword)", 1)
assert feat.effects == weapon_focus("Longsword").effects
def test_weapon_focus_case_insensitive(self) -> None:
feat = _resolve_feat("weapon focus (dagger)", 1)
assert feat.effects == weapon_focus("dagger").effects
def test_unknown_feat_returns_minimal_spec(self) -> None:
feat = _resolve_feat("Lateral Thinking", 1)
assert feat.name == "Lateral Thinking"
assert feat.category == "feat"
assert feat.effects == ()
def test_unknown_feat_preserves_whitespace_stripping(self) -> None:
feat = _resolve_feat(" Some Feat ", 1)
assert feat.name == "Some Feat"
def test_dodge_and_alertness(self) -> None:
assert _resolve_feat("Dodge", 1) is DODGE
# ── _resolve_features ─────────────────────────────────────────────────────────
class TestResolveFeatures:
def test_filters_empty_and_whitespace(self) -> None:
result = _resolve_features(["Power Attack", "", " ", "Dodge"], 1)
assert len(result) == 2
assert result[0] is POWER_ATTACK
assert result[1] is DODGE
def test_filters_non_strings(self) -> None:
result = _resolve_features(["Iron Will", 42, None, True], 1) # type: ignore[list-item]
assert len(result) == 1
def test_empty_list(self) -> None:
assert _resolve_features([], 1) == []
def test_all_unknown(self) -> None:
result = _resolve_features(["Feat A", "Feat B"], 1)
assert len(result) == 2
assert all(f.effects == () for f in result)
# ── load_combatant features+spells ────────────────────────────────────────────
def _write(tmp_path: Path, payload: dict[str, object]) -> Path:
target = tmp_path / "combatant.json"
target.write_text(json.dumps(payload), encoding="utf-8")
return target
def _minimal_payload(**extra: object) -> dict[str, object]:
base: dict[str, object] = {
"name": "Tester",
"level": 3,
"abilities": {
"str_score": 10, "dex_score": 10, "con_score": 10,
"int_score": 10, "wis_score": 10, "cha_score": 10,
},
"hp_max": 10,
"ac": {"total": 10, "touch": 10, "flat_footed": 10},
"bab": 1,
"initiative_mod": 0,
"speed_land_ft": 30,
"saves": {"fort": 0, "ref": 0, "will": 0},
"attacks": [],
}
base.update(extra)
return base
class TestFeatures:
def test_known_feats_resolved(self, tmp_path: Path) -> None:
data = _minimal_payload(features=["Power Attack", "Iron Will"])
c = load_combatant(_write(tmp_path, data))
assert len(c.features) == 2
assert c.features[0] is POWER_ATTACK
assert c.features[1] is IRON_WILL
def test_unknown_feat_preserved(self, tmp_path: Path) -> None:
data = _minimal_payload(features=["Some Unknown Feat"])
c = load_combatant(_write(tmp_path, data))
assert len(c.features) == 1
assert c.features[0].name == "Some Unknown Feat"
def test_toughness_receives_level(self, tmp_path: Path) -> None:
data = _minimal_payload(level=7, features=["Toughness"])
c = load_combatant(_write(tmp_path, data))
assert c.features[0].effects == toughness(7).effects
def test_level_default_one(self, tmp_path: Path) -> None:
data = _minimal_payload(level=1, features=["Toughness"])
c = load_combatant(_write(tmp_path, data))
assert c.features[0].effects == toughness(1).effects
def test_no_features_field(self, tmp_path: Path) -> None:
data = _minimal_payload()
c = load_combatant(_write(tmp_path, data))
assert c.features == []
def test_empty_features_list(self, tmp_path: Path) -> None:
data = _minimal_payload(features=[])
c = load_combatant(_write(tmp_path, data))
assert c.features == []
class TestSpells:
def test_spells_passed_through(self, tmp_path: Path) -> None:
data = _minimal_payload(spells=["Magic Missile", "Shield"])
c = load_combatant(_write(tmp_path, data))
assert c.spells == ["Magic Missile", "Shield"]
def test_no_spells_field(self, tmp_path: Path) -> None:
data = _minimal_payload()
c = load_combatant(_write(tmp_path, data))
assert c.spells == []
def test_empty_spells_list(self, tmp_path: Path) -> None:
data = _minimal_payload(spells=[])
c = load_combatant(_write(tmp_path, data))
assert c.spells == []
class TestBackwardCompatAliases:
def test_monster_load_error_is_combatant_load_error(self) -> None:
assert MonsterLoadError is CombatantLoadError
def test_load_monster_delegates(self, tmp_path: Path) -> None:
data = _minimal_payload(name="Alias Test")
c = load_monster(_write(tmp_path, data))
assert c.name == "Alias Test"