# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: cli-08-camera.e2e.test.ts >> CLI-08 camera follow — pressing KeyD for 300ms pans camera scrollX (Wave 4 GREEN gate)
- Location: test/e2e/cli-08-camera.e2e.test.ts:29:1

# Error details

```
Error: __rebno.cameraScrollX must be populated by GameScene update hook (Plan 06.1-06 S-08)

expect(received).not.toBeNull()

Received: null
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - generic:
    - generic [ref=e5]: Press T or Enter to chat
    - generic [ref=e7]:
      - generic [ref=e8]: Menu
      - button "Resume" [ref=e9] [cursor=pointer]
      - button "Settings" [disabled] [ref=e10]
      - button "Logout" [ref=e11] [cursor=pointer]
    - generic: uat_b
    - generic: uat_a
    - generic: uat_a
    - generic: uat_a
    - generic: uat_a
    - generic: uat_a
    - generic: uat_a
```

# Test source

```ts
  1  | // apps/client/test/e2e/cli-08-camera.e2e.test.ts
  2  | // [int->REQ-CLI-08]
  3  | //
  4  | // Plan 06.1-07 Task 1 — GREEN refinement of the 06.1-03 RED skeleton. Camera
  5  | // follow / scroll assertion against the Wave 3 wire-up (GameScene D6.1-14/15/16
  6  | // applyCameraFollow + Plan 06.1-02 RUN_SPEED_PX_PER_TICK=5 + Pitfall 4
  7  | // accumulator-subtract).
  8  | //
  9  | // Pattern: mirrors `camera-follow.e2e.test.ts` lines 16-89 — login fixture,
  10 | // waitForGameReady, __rebno deterministic hook, keyboard.down/up +
  11 | // waitForTimeout, before/after delta assertion. NO canvas pixel sampling.
  12 | //
  13 | // Threshold derivation (Plan 06.1-07 refinement — 300 ms hold for CI margin):
  14 | //   RUN_SPEED_PX_PER_TICK = 5 (D6.1-05, Phase 06.1)
  15 | //   Sim-tick rate = 30 Hz (CLAUDE.md "Extracted Constants" — speed: 30)
  16 | //   At 30 Hz a 300 ms hold spans ~9 sim ticks → ~45 px of intended motion.
  17 | //   Threshold 15 px keeps a 3× margin to absorb start-up latency,
  18 | //   camera-deadzone delay (D6.1-14 = 32×32 from BNCentral hBorder=304),
  19 | //   and jitter under client prediction. The original 06.1-03 skeleton used
  20 | //   200 ms; we widen to 300 ms here for CI flakiness reduction on slow runners.
  21 | 
  22 | import {
  23 |   test,
  24 |   expect,
  25 |   loginAs,
  26 |   waitForGameReady,
  27 | } from './fixtures.js';
  28 | 
  29 | test('CLI-08 camera follow — pressing KeyD for 300ms pans camera scrollX (Wave 4 GREEN gate)', async ({
  30 |   page,
  31 |   accountA,
  32 |   inviteSuffix,
  33 | }) => {
  34 |   // 1. Login and wait for game-ready (mirrors fixtures S-09 pattern).
  35 |   await loginAs(page, accountA, inviteSuffix);
  36 |   await waitForGameReady(page);
  37 | 
  38 |   // 2. Focus the canvas so key events reach the Phaser input layer.
  39 |   await page.locator('canvas[data-game-ready="true"]').click();
  40 | 
  41 |   // 3. Baseline camera scroll via __rebno deterministic hook (S-08; D6.1-15
  42 |   //    cameraScrollX is published every render frame by GameScene.update).
  43 |   const before = await page.evaluate(
  44 |     () =>
  45 |       (
  46 |         window as unknown as {
  47 |           __rebno?: { cameraScrollX?: number; cameraScrollY?: number };
  48 |         }
  49 |       ).__rebno?.cameraScrollX ?? null,
  50 |   );
  51 |   expect(
  52 |     before,
  53 |     '__rebno.cameraScrollX must be populated by GameScene update hook (Plan 06.1-06 S-08)',
> 54 |   ).not.toBeNull();
     |         ^ Error: __rebno.cameraScrollX must be populated by GameScene update hook (Plan 06.1-06 S-08)
  55 | 
  56 |   // 4. Hold KeyD for 300 ms. Math: RUN_SPEED=5 px/tick x (300 ms / 33.33 ms/tick)
  57 |   //    ~= 45 px of intended motion; deadzone is 32 px so >=13 px of camera scroll
  58 |   //    is expected. Threshold 15 px keeps a 3x margin against the expected
  59 |   //    45 px and absorbs deadzone latency + prediction jitter.
  60 |   await page.keyboard.down('KeyD');
  61 |   await page.waitForTimeout(300);
  62 |   await page.keyboard.up('KeyD');
  63 | 
  64 |   // 5. Brief settle for prediction/reconciliation.
  65 |   await page.waitForTimeout(150);
  66 | 
  67 |   // 6. Post-hold camera scroll (read from the same __rebno.cameraScrollX hook
  68 |   //    that Plan 06.1-06 publishes per render frame — preferred over Phaser
  69 |   //    private state per CONTEXT line "use the rebno-hook field directly").
  70 |   const after = await page.evaluate(
  71 |     () =>
  72 |       (
  73 |         window as unknown as {
  74 |           __rebno?: { cameraScrollX?: number; cameraScrollY?: number };
  75 |         }
  76 |       ).__rebno?.cameraScrollX ?? null,
  77 |   );
  78 | 
  79 |   // 7. Assert camera panned at least 15 px on the x axis (3x margin below
  80 |   //    expected 45 px to absorb 32-px deadzone crossing + prediction jitter).
  81 |   expect(after).not.toBeNull();
  82 |   expect(
  83 |     (after as number) - (before as number),
  84 |     `Camera scrollX did not advance: before=${before} after=${after}`,
  85 |   ).toBeGreaterThan(15);
  86 | });
  87 | 
```