Files
dmtools/tests/Character/CharacterTemplatesTest.php
T
FrankandClaude Sonnet 5 9052218927 Characters: build the page around Darragh Oakwanderer's sheet
Replaces the Characters placeholder with a real 5e character sheet
feature, following the same read-only-detail/edit-split/game-notes
pattern as NPCs and Enemies:

- Character entity: identity, combat vitals, six ability scores, saving
  throw and skill proficiencies (computed modifiers, not stored), a
  small attacks table, and the sheet's freeform boxes (features,
  equipment, personality, appearance, backstory, allies, treasure).
- Detail page shows the portrait uncropped, then the sheet - genuine
  <table>s for ability scores, skills and attacks, since that's the one
  part of a character sheet that actually is a table; everything else
  reuses the prose helpers from the enemy stat block. Game notes are
  the only editable field here, same as Enemy/NPC.
- Edit page covers every field via CharacterType; the attack rows are
  six plain inputs handled by hand (not a Symfony collection field),
  since it's a short fixed-shape table, not a dynamic list.
- Portraits follow the NPC convention: assets/image/characters/<NameNo
  Spaces>.png, computed path, not a DB column.
- Imported the example sheet from assets/characters/ (Darragh
  Oakwanderer, halfling ranger) directly into the database.

Tests: entity math (ability/save/skill modifiers, passive perception,
portrait path), template rendering for show/edit/index (catches broken
Twig/macro/path()/asset() calls), and the login-gate redirect. 21/21
passing; schema validate clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:55:28 +02:00

106 lines
3.9 KiB
PHP

<?php
namespace App\Tests\Character;
use App\Entity\Character;
use App\Form\CharacterGameNotesType;
use App\Form\CharacterType;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
/**
* Renders the character templates end to end (no real HTTP/DB) so a broken
* Twig variable, macro import or path()/asset() call fails loudly.
*/
class CharacterTemplatesTest extends KernelTestCase
{
private function darragh(): Character
{
$c = (new Character())
->setName('Darragh Oakwanderer')
->setPlayerName('PH')
->setRace('Halfling, stout')
->setCharacterClass('Ranger')
->setLevel(2)
->setBackground('Urchin')
->setAlignment('CN')
->setArmorClass(15)
->setInitiative(3)
->setSpeed('25 ft.')
->setHitDice('2d10')
->setStrength(12)->setDexterity(17)->setConstitution(14)
->setIntelligence(12)->setWisdom(13)->setCharisma(8)
->setSavingThrowProficiencies(['Strength', 'Dexterity'])
->setSkillProficiencies(['Sleight of Hand', 'Stealth'])
->setAttacks([['name' => 'Dart', 'bonus' => '+5', 'damage' => '1d4+5', 'range' => '20/60 ft.']])
->setFeaturesTraits("Stout Resilience\nLucky")
->setPersonalityTraits('I eat like a pig and have bad manners');
(new \ReflectionProperty(Character::class, 'id'))->setValue($c, 1);
return $c;
}
private function pushRequestWithSession(): void
{
$request = Request::create('/characters/1');
$request->setSession(new Session(new MockArraySessionStorage()));
self::getContainer()->get('request_stack')->push($request);
}
public function testShowPageRenders(): void
{
self::bootKernel();
$this->pushRequestWithSession();
$c = self::getContainer();
$character = $this->darragh();
$form = $c->get('form.factory')->create(CharacterGameNotesType::class, $character);
$html = $c->get('twig')->render('characters/show.html.twig', [
'character' => $character,
'form' => $form->createView(),
]);
self::assertStringContainsString('Darragh Oakwanderer', $html);
self::assertStringContainsString('Halfling, stout Ranger 2', $html);
self::assertStringContainsString('Stealth', $html);
self::assertStringContainsString('Dart', $html);
self::assertStringContainsString('Game notes', $html);
}
public function testEditPageRendersForNewAndExisting(): void
{
self::bootKernel();
$this->pushRequestWithSession();
$c = self::getContainer();
foreach ([true, false] as $isNew) {
$character = $isNew ? new Character() : $this->darragh();
$form = $c->get('form.factory')->create(CharacterType::class, $character);
$html = $c->get('twig')->render('characters/edit.html.twig', [
'character' => $character,
'form' => $form->createView(),
'is_new' => $isNew,
]);
self::assertStringContainsString($isNew ? 'New character' : 'Edit Darragh Oakwanderer', $html);
self::assertStringContainsString('attacks[0][name]', $html);
}
}
public function testIndexPageRendersEmptyAndPopulated(): void
{
self::bootKernel();
$this->pushRequestWithSession();
$twig = self::getContainer()->get('twig');
$empty = $twig->render('characters/index.html.twig', ['characters' => []]);
self::assertStringContainsString('No party members yet', $empty);
$populated = $twig->render('characters/index.html.twig', ['characters' => [$this->darragh()]]);
self::assertStringContainsString('Darragh Oakwanderer', $populated);
}
}