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>
106 lines
3.9 KiB
PHP
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);
|
|
}
|
|
}
|