P20: map viewport zoom/pan/fit + token placement palette
Render maps at natural resolution inside a CSS-transform world layer (zoom, pan, fit-to-view) instead of shrinking to maxWidth — large maps are now usable. Pure viewport math in src/lib/map/viewport.ts (unit-tested). New TokenPalette places party PCs / active-encounter combatants / blanks in one click with dup detection; tokens gain optional combatantId (Dexie v9) for live encounter HP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { createCharacter } from './helpers';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -36,3 +37,48 @@ test('maps: upload, place token, reveal all, show to players', async ({ page })
|
||||
await expect(page.getByRole('heading', { name: 'Battle map' })).toBeVisible();
|
||||
await expect(page.getByTestId('map-token')).toHaveCount(1);
|
||||
});
|
||||
|
||||
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 world = page.getByTestId('map-world');
|
||||
await expect(world).toBeVisible();
|
||||
// World layer is sized to the image's natural pixels (not shrunk to a cap).
|
||||
await expect(world).toHaveCSS('width', '512px');
|
||||
|
||||
const scaleOf = async () => {
|
||||
const t = await world.evaluate((el) => getComputedStyle(el).transform);
|
||||
const m = /matrix\(([^,]+)/.exec(t);
|
||||
return m ? Number(m[1]) : 1;
|
||||
};
|
||||
const before = await scaleOf();
|
||||
await page.getByRole('button', { name: 'Zoom in' }).click();
|
||||
await page.getByRole('button', { name: 'Zoom in' }).click();
|
||||
expect(await scaleOf()).toBeGreaterThan(before);
|
||||
|
||||
await page.getByRole('button', { name: 'Fit map to view' }).click();
|
||||
expect(await scaleOf()).toBeCloseTo(before, 1);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user