Files
ttrpg_manager/e2e-realtime/seats.spec.ts
T
NilsBriggen 360d9ff842 P21: player seats + two-way live play + offline handoff
Players claim a seat (GM approves) and drive their own character live —
HP/slots/resources/conditions/dice broadcast to the table — via new seat
protocol messages and per-seat ownership enforced in RoomHub. GM persists
player edits (charactersRepo.update → liveQuery → rebroadcast). Offline dev:
"Share with player" encodes a claim link (src/lib/sync/playerLink.ts) → /player
imports the character locally for full-sheet editing, bundled back as an
offlineSnapshot on seat claim for GM review.

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

52 lines
2.3 KiB
TypeScript
Raw 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.
const gm = await fresh(gmCtx);
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.locator('div.rounded-lg', { 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();
});