From 81224829a24dc2447c554727844c5c6210427ca8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 31 May 2026 19:21:24 +0200 Subject: [PATCH] test(stammbaum): prove the AC8 mobile-centre wiring at the route layer (#703) Sara/Elicit noted AC8 was proven only as recentreAbove geometry, never as wired behaviour. Add route-level tests that mock window.matchMedia: a tap recentres the canvas (mirror effect re-fires) when the mobile breakpoint matches, and leaves the view untouched on desktop where the side panel is a flex sibling that never overlaps the canvas. Co-Authored-By: Claude Opus 4.8 --- .../src/routes/stammbaum/page.svelte.test.ts | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/stammbaum/page.svelte.test.ts b/frontend/src/routes/stammbaum/page.svelte.test.ts index b9f58e20..578aff76 100644 --- a/frontend/src/routes/stammbaum/page.svelte.test.ts +++ b/frontend/src/routes/stammbaum/page.svelte.test.ts @@ -24,7 +24,28 @@ vi.mock('$app/navigation', () => ({ goto: vi.fn() })); -afterEach(cleanup); +// The page reads window.matchMedia('(max-width: 767px)').matches at init to +// decide whether to centre a tapped person above the bottom sheet (#703 AC8). +// Make the mock query-aware so only the mobile breakpoint flips; other media +// queries (e.g. prefers-reduced-motion) keep their benign default. +const originalMatchMedia = window.matchMedia; +function mockMatchMedia(isMobile: boolean) { + window.matchMedia = ((query: string) => ({ + matches: query.includes('max-width') ? isMobile : false, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false + })) as unknown as typeof window.matchMedia; +} + +afterEach(() => { + cleanup(); + window.matchMedia = originalMatchMedia; +}); async function loadComponent() { return (await import('./+page.svelte')).default; @@ -35,6 +56,12 @@ const sampleNodes = [ { id: 'p-2', firstName: 'Bert', lastName: 'Schmidt', displayName: 'Bert Schmidt' } ]; +// Typed family nodes for the AC8 tests — familyMember is required on the DTO. +const familyNodes = [ + { id: 'p-1', displayName: 'Anna Schmidt', familyMember: true }, + { id: 'p-2', displayName: 'Bert Schmidt', familyMember: true } +]; + describe('stammbaum page', () => { it('shows the empty state when there are no family nodes', async () => { mockPage.url = new URL('http://localhost/stammbaum'); @@ -125,4 +152,44 @@ describe('stammbaum page', () => { expect(url.searchParams.has('cx')).toBe(true); expect(url.searchParams.has('cy')).toBe(true); }); + + // AC8 — the tapped person must clear the bottom sheet on a phone, but the + // desktop side panel is a flex sibling that never overlaps the canvas, so no + // centring should fire there. These tests prove the matchMedia gate around + // selectPerson, not just the recentreAbove geometry (covered in panZoom.test). + it('recentres the tapped person when matchMedia reports mobile (#703 AC8)', async () => { + mockMatchMedia(true); + mockPage.url = new URL('http://localhost/stammbaum'); + const Stammbaum = await loadComponent(); + render(Stammbaum, { + props: { data: { nodes: familyNodes, edges: [], initialView: DEFAULT_VIEW } } + }); + // Let the mount-time URL mirror settle, then isolate the tap's effect. + await vi.waitFor(() => expect(replaceState).toHaveBeenCalled()); + replaceState.mockClear(); + + await page.getByRole('button', { name: 'Anna Schmidt' }).click(); + + // The mobile tap recentres the canvas → the view changes → the ?cx&cy&z + // mirror effect re-fires. (Desktop, below, leaves the view untouched.) + await vi.waitFor(() => expect(replaceState).toHaveBeenCalled()); + }); + + it('does not recentre on tap when matchMedia reports desktop (#703 AC8)', async () => { + mockMatchMedia(false); + mockPage.url = new URL('http://localhost/stammbaum'); + const Stammbaum = await loadComponent(); + render(Stammbaum, { + props: { data: { nodes: familyNodes, edges: [], initialView: DEFAULT_VIEW } } + }); + await vi.waitFor(() => expect(replaceState).toHaveBeenCalled()); + replaceState.mockClear(); + + await page.getByRole('button', { name: 'Anna Schmidt' }).click(); + + // The tap registers — the desktop side panel opens — but no recentre fires, + // so the view never changes and the mirror effect stays silent. + await expect.element(page.getByRole('complementary')).toBeVisible(); + expect(replaceState).not.toHaveBeenCalled(); + }); });