99c7657f96
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>
193 lines
9.4 KiB
TypeScript
193 lines
9.4 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { createCharacter } from './helpers';
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.evaluate(async () => {
|
|
indexedDB.deleteDatabase('ttrpg-manager');
|
|
localStorage.clear();
|
|
});
|
|
await page.reload();
|
|
});
|
|
|
|
test('maps: upload, place token, reveal all, show to players', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('VTT');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
|
|
// Editor loads (Move tool present)
|
|
await expect(page.getByRole('button', { name: 'move', exact: true })).toBeVisible();
|
|
|
|
// Add a token
|
|
await page.getByRole('button', { name: '+ Token' }).click();
|
|
await expect(page.getByTestId('map-token')).toHaveCount(1);
|
|
|
|
// Reveal tool exposes the contextual fog controls; reveal all
|
|
await page.getByRole('button', { name: 'reveal', exact: true }).click();
|
|
await page.getByText('Fog', { exact: true }).locator('input[type=checkbox]').check();
|
|
await page.getByRole('button', { name: 'Reveal all' }).click();
|
|
|
|
// Show to players, then the player view renders the battle map
|
|
await page.getByRole('button', { name: 'Show to players' }).click();
|
|
await page.getByRole('link', { name: 'Open player view' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Battle map' })).toBeVisible();
|
|
await expect(page.getByTestId('map-token')).toHaveCount(1);
|
|
});
|
|
|
|
test('maps: import a Universal VTT (.dd2vtt) map', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('VTTimport');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
|
|
await page.locator('input[type="file"]').setInputFiles('e2e/fixtures/sample.dd2vtt');
|
|
|
|
// Imported map opens in the editor with the file's pixels_per_grid as the grid size.
|
|
await expect(page.getByTestId('map-viewport')).toBeVisible();
|
|
await expect(page.getByLabel('Grid cell size')).toHaveValue('100');
|
|
await expect(page.getByRole('button', { name: 'Export .uvtt' })).toBeVisible();
|
|
});
|
|
|
|
test('maps: combat HUD drives turns + token HP syncs to the tracker', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Fight');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
|
|
await page.getByRole('link', { name: 'Characters' }).click();
|
|
await createCharacter(page, 'Hero');
|
|
|
|
// Encounter with the hero + a monster, then start combat.
|
|
await page.getByRole('link', { name: 'Combat' }).click();
|
|
await page.getByRole('button', { name: '+ New encounter' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Brawl');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByLabel('Name').fill('Goblin');
|
|
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
|
await page.locator('select').filter({ has: page.locator('option', { hasText: 'Choose…' }) }).selectOption({ label: 'Hero (Lv 1)' });
|
|
await page.getByRole('button', { name: 'Start combat' }).click();
|
|
|
|
// Map: the combat HUD appears and Next turn works.
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
await expect(page.getByText(/Combat · Round 1/)).toBeVisible();
|
|
await page.getByRole('button', { name: /Next turn/ }).click();
|
|
await expect(page.getByText(/Combat · Round/)).toBeVisible();
|
|
|
|
// Place the Goblin combatant; its token edit shows the synced Combat HP control.
|
|
await page.getByRole('button', { name: /Goblin/ }).click();
|
|
await page.getByTestId('map-token').first().click();
|
|
await expect(page.getByText(/Combat HP/)).toBeVisible();
|
|
});
|
|
|
|
test('maps: walls tool + dynamic vision controls', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Vision');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('e2e/fixtures/sample.dd2vtt');
|
|
await expect(page.getByTestId('map-viewport')).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: 'walls', exact: true }).click();
|
|
await expect(page.getByRole('button', { name: /Reveal from party/ })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Clear walls/ })).toBeVisible();
|
|
|
|
const vision = page.locator('label', { hasText: 'Vision' }).locator('input[type=checkbox]');
|
|
await vision.check();
|
|
await expect(vision).toBeChecked();
|
|
});
|
|
|
|
test('maps: zoom controls scale the canvas and fit resets', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Zoom');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
|
|
const viewport = page.getByTestId('map-viewport');
|
|
await expect(viewport).toBeVisible();
|
|
const scaleOf = async () => Number(await viewport.getAttribute('data-zoom'));
|
|
|
|
// Let the initial fit settle, then zooming in must increase the zoom…
|
|
await page.getByRole('button', { name: 'Fit map to view' }).click();
|
|
const fitZoom = await scaleOf();
|
|
expect(fitZoom).toBeGreaterThan(0);
|
|
await page.getByRole('button', { name: 'Zoom in' }).click();
|
|
await page.getByRole('button', { name: 'Zoom in' }).click();
|
|
const zoomedIn = await scaleOf();
|
|
expect(zoomedIn).toBeGreaterThan(fitZoom);
|
|
|
|
// …and Fit brings it back to the fit value.
|
|
await page.getByRole('button', { name: 'Fit map to view' }).click();
|
|
expect(await scaleOf()).toBeCloseTo(fitZoom, 5);
|
|
});
|
|
|
|
test('maps: place a token from a planned encounter via the palette', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Enc');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
|
|
// A planned (not-yet-started) encounter with a monster.
|
|
await page.getByRole('link', { name: 'Combat' }).click();
|
|
await page.getByRole('button', { name: '+ New encounter' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Ambush');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
await page.getByLabel('Name').fill('Bandit');
|
|
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
|
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
|
|
// The palette's Encounter section surfaces the planned encounter's combatant.
|
|
await expect(page.getByText('Encounter', { exact: true })).toBeVisible();
|
|
await page.getByRole('button', { name: /Bandit/ }).click();
|
|
await expect(page.getByTestId('map-token')).toHaveCount(1);
|
|
await expect(page.getByTestId('map-token')).toContainText('Bandit');
|
|
});
|
|
|
|
test('maps: a character portrait becomes the token icon', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Art');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
|
|
await page.getByRole('link', { name: 'Characters' }).click();
|
|
await createCharacter(page, 'Aria');
|
|
// Upload a portrait on the sheet; it renders in the header avatar.
|
|
await page.getByLabel('Portrait').setInputFiles('public/pwa-512x512.png');
|
|
await expect(page.locator('label img').first()).toBeVisible();
|
|
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
|
|
// Placing the PC drops a token that shows the portrait image.
|
|
await page.getByRole('button', { name: /Aria/ }).click();
|
|
await expect(page.getByTestId('map-token').first().locator('img')).toBeVisible();
|
|
});
|
|
|
|
test('maps: place a party token from the palette', async ({ page }) => {
|
|
await page.getByRole('button', { name: '+ New campaign' }).first().click();
|
|
await page.locator('input[data-autofocus]').fill('Party');
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
|
|
await page.getByRole('link', { name: 'Characters' }).click();
|
|
await createCharacter(page, 'Aria');
|
|
|
|
await page.getByRole('link', { name: 'Dashboard' }).click();
|
|
await page.getByRole('link', { name: 'Maps' }).click();
|
|
await page.locator('input[type="file"]').setInputFiles('public/pwa-512x512.png');
|
|
|
|
// Palette lists the PC; clicking it drops a linked token.
|
|
await page.getByRole('button', { name: /Aria/ }).click();
|
|
await expect(page.getByTestId('map-token')).toHaveCount(1);
|
|
await expect(page.getByTestId('map-token')).toContainText('Aria');
|
|
});
|