# 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.e2e.test.ts >> CLI-08 hard milestone — two clients see each other move + chat round-trip + reconnect grace
- Location: test/e2e/cli-08.e2e.test.ts:22:1

# Error details

```
Error: expect(locator).toBeAttached() failed

Locator: locator('[data-nameplate="uat_b"]')
Expected: attached
Error: strict mode violation: locator('[data-nameplate="uat_b"]') resolved to 2 elements:
    1) <div data-x-coord="500" data-y-coord="472" data-nameplate="uat_b">uat_b</div> aka getByText('uat_b').first()
    2) <div data-x-coord="500" data-y-coord="472" data-nameplate="uat_b">uat_b</div> aka getByText('uat_b').nth(1)

Call log:
  - Expect "toBeAttached" with timeout 10000ms
  - waiting for locator('[data-nameplate="uat_b"]')

```

# Test source

```ts
  1   | // apps/client/test/e2e/cli-08.e2e.test.ts
  2   | // [int->REQ-CLI-08]
  3   | // Plan 06-08 Task 2 — CLI-08 hard-milestone two-client smoke. The MVP merge
  4   | // gate. Was committed RED on plan 06-01 (test.skip(true, ...)); plan 06-08
  5   | // flips the skip and turns the assertions GREEN by:
  6   | //   - LoginScene DOM form (plan 06-04 / forms/login.html)
  7   | //   - GameScene WS join + state apply + render + chat HUD (plan 06-07)
  8   | //   - Authoritative server (Phase 4) + deployed staging (Phase 5)
  9   | //
  10  | // Two contexts = two independent users (cookies, storage, WS). The test
  11  | // asserts:
  12  | //   1. Both clients reach the GameScene canvas (data-game-ready=true)
  13  | //   2. Each client sees the OTHER nameplate (DOM-mirror per remote player)
  14  | //   3. A presses 'd' for 1s; B sees A's data-x-coord increase (movement
  15  | //      round-trip via authoritative server broadcast)
  16  | //   4. A types in chat; B sees the chat-line in its DOM mirror
  17  | //   5. A force-closes the WS; Colyseus auto-reconnect within grace returns
  18  | //      A to data-game-ready=true (Phase 4 D-12 reconnection grace)
  19  | 
  20  | import { test, expect, loginAs, waitForGameReady } from './fixtures.js';
  21  | 
  22  | test('CLI-08 hard milestone — two clients see each other move + chat round-trip + reconnect grace', async ({
  23  |   browser,
  24  |   accountA,
  25  |   accountB,
  26  |   inviteSuffix,
  27  | }) => {
  28  |   const ctxA = await browser.newContext();
  29  |   const ctxB = await browser.newContext();
  30  |   const a = await ctxA.newPage();
  31  |   const b = await ctxB.newPage();
  32  | 
  33  |   try {
  34  |     // (1) Both clients log in and reach GameScene.
  35  |     await loginAs(a, accountA, inviteSuffix);
  36  |     await loginAs(b, accountB, inviteSuffix);
  37  |     await waitForGameReady(a);
  38  |     await waitForGameReady(b);
  39  | 
  40  |     // (2) Each client sees the OTHER's nameplate (DOM-mirror per remote
  41  |     //     player; plan 06-07 Nameplate.ts emits `[data-nameplate=<username>]`).
  42  |     //     The mirror is intentionally hidden (display:none) per UI-SPEC §
  43  |     //     Nameplate — the visible label lives in the Phaser canvas. Assert
  44  |     //     attached-to-DOM instead of visibility.
  45  |     await expect(
  46  |       a.locator(`[data-nameplate="${accountB.username}"]`),
> 47  |     ).toBeAttached({ timeout: 10_000 });
      |       ^ Error: expect(locator).toBeAttached() failed
  48  |     await expect(
  49  |       b.locator(`[data-nameplate="${accountA.username}"]`),
  50  |     ).toBeAttached({ timeout: 10_000 });
  51  | 
  52  |     // (3) A presses 'd' for 1s; B observes A's data-x-coord advance.
  53  |     const beforeXStr = await b
  54  |       .locator(`[data-nameplate="${accountA.username}"]`)
  55  |       .getAttribute('data-x-coord');
  56  |     const beforeX = Number(beforeXStr ?? '0');
  57  |     expect(Number.isFinite(beforeX)).toBe(true);
  58  | 
  59  |     // Focus A's canvas so keypress reaches the Phaser input layer.
  60  |     await a.locator('canvas[data-game-ready="true"]').click();
  61  |     await a.keyboard.down('d');
  62  |     await a.waitForTimeout(1_000);
  63  |     await a.keyboard.up('d');
  64  |     // Allow ~3 server ticks (50ms each at 20Hz) for B to receive the
  65  |     // authoritative broadcast.
  66  |     await b.waitForTimeout(500);
  67  | 
  68  |     const afterXStr = await b
  69  |       .locator(`[data-nameplate="${accountA.username}"]`)
  70  |       .getAttribute('data-x-coord');
  71  |     const afterX = Number(afterXStr ?? '0');
  72  |     // Diagnostic surface — pin down whether step (3) failure is on the
  73  |     // input-dispatch side (no axes event fired locally) or server-side
  74  |     // (server didn't broadcast a new x).
  75  |     const aLastAxes = await a.evaluate(
  76  |       () => (window as unknown as { rebno?: { lastInputAxes?: unknown; lastInputSeq?: unknown } }).rebno,
  77  |     );
  78  |     // eslint-disable-next-line no-console
  79  |     console.log(
  80  |       `[cli-08 diag] beforeX=${beforeX} afterX=${afterX} A.lastInput=${JSON.stringify(aLastAxes)}`,
  81  |     );
  82  |     expect(afterX).toBeGreaterThan(beforeX + 30); // ≥30 px movement; conservative.
  83  | 
  84  |     // (4) Chat round-trip. A presses Enter to focus chat, types, Enter to send.
  85  |     //     B sees a `.chat-line` matching `<sender>: <text>` within 5s.
  86  |     await a.keyboard.press('Enter');
  87  |     await a.locator('[data-chat-input]').waitFor({ timeout: 5_000 });
  88  |     await a.fill('[data-chat-input]', 'hello from A');
  89  |     await a.locator('[data-chat-input]').press('Enter');
  90  |     await expect(
  91  |       b
  92  |         .locator('.chat-line')
  93  |         .filter({ hasText: `${accountA.username}: hello from A` }),
  94  |     ).toBeVisible({ timeout: 5_000 });
  95  | 
  96  |     // (5) Reconnect grace — kill A's WS, wait, expect data-game-ready=true again.
  97  |     //     Colyseus auto-reconnects via the cached reconnectionToken (Phase 4 D-12).
  98  |     await a.evaluate(() => {
  99  |       const w = window as unknown as {
  100 |         __rebno?: { room?: { connection?: { close?: () => void } } };
  101 |       };
  102 |       w.__rebno?.room?.connection?.close?.();
  103 |     });
  104 |     // Briefly drop ready; auto-reconnect re-applies state and re-asserts ready.
  105 |     await a.waitForTimeout(2_000);
  106 |     await expect(
  107 |       a.locator('canvas[data-game-ready="true"]'),
  108 |     ).toBeVisible({ timeout: 15_000 });
  109 |   } finally {
  110 |     await ctxA.close();
  111 |     await ctxB.close();
  112 |   }
  113 | });
  114 | 
```