Files
ttrpg_manager/e2e-realtime/seats.spec.ts
NilsBriggen 99c7657f96 UI overhaul "Living Codex" E: per-screen layouts
Ported all 9 screens to the prototype layout (visual only; all data wiring, text,
aria-labels, and testids preserved) via parallel agents + central verification:
- Dashboard: editorial hero, party roster w/ HP meters + avatars, quests, threats, rolls.
- Campaigns: cover-art deck cards + gilt rule + footer.
- Character sheet + roster: hero header, StatCoin abilities, prof rows; grimoire cards.
- Combat: glowing initiative rows (PC gilt / foe ember), condition badges, log.
- Dice: die-pool glyphs, adv/dis segmented toggle, crit/fumble stage, history.
- Compendium: Spectral stat blocks (ember headers, ability row).
- Settings: grouped icon-tile cards. Live Session: room-code card, seat grid, player board.
- Battle Map: carded list + tooled editor chrome (canvas untouched).

Regression fixes after the ports: dice die-buttons aria-label `d4` not `Roll 1d4`
(was colliding with the primary Roll button); map "Open player view" link uses an
ExternalLink icon (test updated, ↗ glyph removed); seat-claim card gets a stable
data-testid="seat-option" (redesign moved rounded-lg→rounded-xl).

223 unit + 34 e2e + 2 realtime green; tsc + eslint + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:37:00 +02:00

53 lines
2.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, type BrowserContext } from '@playwright/test';
async function fresh(ctx: BrowserContext) {
const page = await ctx.newPage();
await page.goto('/');
await page.evaluate(() => { indexedDB.deleteDatabase('ttrpg-manager'); localStorage.clear(); });
await page.reload();
return page;
}
test('a player claims a seat and manages their character; edits round-trip to the GM', async ({ browser }) => {
const gmCtx = await browser.newContext();
const playerCtx = await browser.newContext();
// GM: sample campaign + host (hosting requires a signed-in GM).
const gm = await fresh(gmCtx);
await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM'));
gm.on('dialog', (d) => d.dismiss()); // optional-password prompt
await gm.getByLabel('Settings').click();
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
await expect(gm.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible();
await gm.getByRole('button', { name: '📡 Host' }).click();
const code = (await gm.getByTestId('join-code').textContent())?.replace(/[^A-Z0-9]/g, '') ?? '';
expect(code).toHaveLength(6);
// Player: join → seat-claim screen → claim Lia.
const player = await playerCtx.newPage();
await player.goto(`/play?room=${code}`);
await expect(player.getByRole('heading', { name: 'Which character is yours?' })).toBeVisible();
await player.getByTestId('seat-option').filter({ hasText: 'Lia the Brave' }).getByRole('button', { name: 'This is me' }).click();
// GM: approve the seat request.
await gm.getByTestId('seat-requests').click();
await gm.getByRole('button', { name: 'Grant', exact: true }).click();
// Player: now drives an interactive sheet.
await expect(player.getByTestId('my-character')).toBeVisible();
const hp = await player.getByTestId('my-hp').textContent();
const cur = Number((hp ?? '0/0').split('/')[0]);
expect(cur).toBeGreaterThan(5);
// Spend HP → optimistic update on the panel…
await player.getByRole('button', { name: '5' }).click();
await expect(player.getByTestId('my-hp')).toHaveText(new RegExp(`^${cur - 5}/`));
// …and it round-trips: GM persists it and re-broadcasts to the party board.
const party = player.locator('section').filter({ has: player.getByRole('heading', { name: 'Party' }) });
await expect(party.getByText(`${cur - 5}/`)).toBeVisible({ timeout: 8000 });
await gmCtx.close();
await playerCtx.close();
});