diff --git a/frontend/e2e/stammbaum-mobile.visual.spec.ts b/frontend/e2e/stammbaum-mobile.visual.spec.ts new file mode 100644 index 00000000..6de8c48b --- /dev/null +++ b/frontend/e2e/stammbaum-mobile.visual.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; + +// Visual + structural coverage for the #692 mobile read path (pan/zoom/fit, +// first-load affordance, bottom-sheet person panel). +// +// Snapshot assertions are gated on VISUAL=1 because they need pre-captured +// baselines — regenerate in CI with `playwright test --update-snapshots` after +// intentional UI changes. Structural assertions run unconditionally. The whole +// suite is also subject to the project-wide Chromium-in-CI gate (#363); it +// captures new snapshots rather than replacing the #361 desktop baselines. +const VISUAL = process.env.VISUAL === '1'; + +const WIDTHS = [320, 414, 768] as const; + +test.describe('Stammbaum — mobile read path (#692)', () => { + // Touch emulation so the canvas reports pointer:coarse and the first-load + // affordance appears; reduced-motion is already forced project-wide. + test.use({ hasTouch: true, isMobile: true }); + + for (const width of WIDTHS) { + test(`affordance + controls render at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 720 }); + await page.addInitScript(() => localStorage.removeItem('stammbaumAffordanceDismissedAt')); + await page.goto('/stammbaum'); + await expect(page.getByRole('heading', { name: 'Stammbaum' })).toBeVisible(); + + const empty = page.getByRole('heading', { name: 'Noch keine Familienmitglieder' }); + if (await empty.isVisible().catch(() => false)) { + test.skip(true, 'no seeded family tree in this environment'); + } + + // Bottom-right control cluster with the fit-to-screen affordance. + await expect(page.getByTestId('fit-to-screen')).toBeVisible(); + // First-load interactive hint (touch only). + await expect(page.getByRole('status')).toBeVisible(); + + if (VISUAL) { + await expect(page).toHaveScreenshot(`stammbaum-affordance-${width}.png`, { + animations: 'disabled' + }); + } + }); + } + + test('bottom sheet opens on node tap at 414px and preserves the canvas', async ({ page }) => { + await page.setViewportSize({ width: 414, height: 720 }); + await page.goto('/stammbaum'); + await expect(page.getByRole('heading', { name: 'Stammbaum' })).toBeVisible(); + + const node = page.locator('svg[aria-label="Stammbaum"] g[role="button"]').first(); + if ((await node.count()) === 0) test.skip(true, 'no seeded nodes to tap'); + + await node.tap(); + const sheet = page.getByRole('dialog'); + await expect(sheet).toBeVisible(); + + if (VISUAL) { + await expect(page).toHaveScreenshot('stammbaum-bottom-sheet-414.png', { + animations: 'disabled' + }); + } + + // Dismiss via the backdrop and confirm the sheet closes (state survives). + await page.getByRole('button', { name: 'Schließen' }).first().click(); + await expect(sheet).toBeHidden(); + }); +});