` from `localStorage`; the bundled list is server-rendered and not replaced by JS.
- [ ] **Step 1: Add a new test for the bundled list**
Add the following test method to `tests/Integration/GetHomePageTest.php`, immediately before the closing brace of the class. The existing `testItReturnsTheHomePageWithSecurityHeaders` test remains unchanged.
```php
public function testItRendersTheBundledScenariosListFromTheManifest(): void
{
$tmpDir = sys_get_temp_dir() . '/bf-home-bundled-' . bin2hex(random_bytes(4));
mkdir($tmpDir, 0700, true);
file_put_contents(
$tmpDir . '/manifest.json',
json_encode([
'skirmish' => ['name' => 'Skirmish', 'summary' => 'Open ground.'],
'hold-the-line' => ['name' => 'Hold the Line', 'summary' => 'Defend.'],
], JSON_THROW_ON_ERROR),
);
foreach (['skirmish.json', 'hold-the-line.json'] as $file) {
file_put_contents($tmpDir . '/' . $file, '{}');
}
$handler = new GetHomePage($tmpDir);
$request = new Request([], [], [], [], [], '', 'GET', '/', 'text/html', '');
$response = $handler->handle($request, []);
self::assertSame(200, $response->status);
self::assertStringContainsString('id="bf-bundled"', $response->body);
self::assertStringContainsString('data-bf-bundle="skirmish"', $response->body);
self::assertStringContainsString('data-bf-bundle="hold-the-line"', $response->body);
self::assertStringContainsString('Open ground.', $response->body);
self::assertStringContainsString('Defend.', $response->body);
unlink($tmpDir . '/manifest.json');
foreach (['skirmish.json', 'hold-the-line.json'] as $file) {
unlink($tmpDir . '/' . $file);
}
rmdir($tmpDir);
}
```
Add a new test `testItReturnsTheHomePageWithSecurityHeaders` next to the existing one — that one is no longer the only test. Keep the old test intact (it still passes — the existing assertions are still true on the new view). Wrap the existing test in a separate method, and keep its body as is. The file's structure should now contain two test methods.
- [ ] **Step 2: Run the test to verify it fails**
Run: `vendor/bin/phpunit tests/Integration/GetHomePageTest.php`
Expected: FAIL on the new test because `GetHomePage` does not yet accept a scenarios dir and the home view does not render the bundled list.
- [ ] **Step 3: Update `GetHomePage` to accept the scenarios dir and pass it to the view**
Replace `src/Http/Handlers/GetHomePage.php` with:
```php
$params */
public function handle(Request $request, array $params): Response
{
$csrf = $request->cookies['__csrf'] ?? '';
$bundled = $this->loadBundledScenarios();
ob_start();
require_once __DIR__ . '/../../Views/layout.php';
require __DIR__ . '/../../Views/home.php';
$body = (string) ob_get_clean();
return Response::html(200, $body);
}
/** @return list
*/
private function loadBundledScenarios(): array
{
if ($this->scenariosDir === '' || !is_file($this->scenariosDir . '/manifest.json')) {
return [];
}
$decoded = json_decode((string) file_get_contents($this->scenariosDir . '/manifest.json'), true);
if (!is_array($decoded)) {
return [];
}
$entries = [];
foreach ($decoded as $id => $entry) {
if (!is_string($id) || !preg_match('/^[a-z0-9-]+$/', $id) || !is_array($entry)) {
continue;
}
if (!isset($entry['name'], $entry['summary'])) {
continue;
}
if (!is_file($this->scenariosDir . '/' . $id . '.json')) {
continue;
}
$entries[] = [
'id' => $id,
'name' => (string) $entry['name'],
'summary' => (string) $entry['summary'],
];
}
usort($entries, static fn (array $a, array $b): int => strcmp($a['id'], $b['id']));
return $entries;
}
}
```
Note: the `$bundled` local variable inside `handle` is captured by the `ob_start` / `require` block (the view runs in `handle`'s scope), so the view sees the local without any explicit pass. No further wiring is required.
- [ ] **Step 4: Update the home view to render the bundled list**
Replace `src/Views/home.php` with:
```php
$bundled Provided by GetHomePage. */
?> Escape::html($v);
$a = static fn (string $v): string => Escape::attr($v);
?>
BattleForge
New scenario
Play bundled
-
Recent scenarios
)` with:
- The shared `layout.php` chrome.
- A header bar: `