From 4ebebe1e0743eb43a445456af0e09ce1068ad139 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 31 May 2026 19:44:19 +0200 Subject: [PATCH] test(stammbaum): assert AC8 recentre via viewBox, not replaceState (#703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop AC8 test flaked in CI: it asserted replaceState was never called after a tap, but the mount-time URL mirror fired late with the unchanged default view (cx=0&cy=0&z=1), tripping the assertion. Assert on the rendered viewBox instead — a pure function of the view state — so a recentre shows as a shifted origin and a desktop tap leaves it identical, with no dependence on the noisy mirror-effect timing. Co-Authored-By: Claude Opus 4.8 --- .../src/routes/stammbaum/page.svelte.test.ts | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/frontend/src/routes/stammbaum/page.svelte.test.ts b/frontend/src/routes/stammbaum/page.svelte.test.ts index 578aff76..baa1450f 100644 --- a/frontend/src/routes/stammbaum/page.svelte.test.ts +++ b/frontend/src/routes/stammbaum/page.svelte.test.ts @@ -157,6 +157,15 @@ describe('stammbaum page', () => { // 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). + // We assert on the rendered viewBox — a pure function of the view state — so a + // recentre is observed as a shifted origin, with no dependence on the noisy + // mount-time URL-mirror timing. + function svgViewBox(): string { + const svg = document.querySelector('svg[aria-label="Stammbaum"]'); + if (!svg) throw new Error('No Stammbaum svg rendered'); + return svg.getAttribute('viewBox') ?? ''; + } + it('recentres the tapped person when matchMedia reports mobile (#703 AC8)', async () => { mockMatchMedia(true); mockPage.url = new URL('http://localhost/stammbaum'); @@ -164,15 +173,17 @@ describe('stammbaum page', () => { 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(); + const before = await vi.waitFor(() => { + const vb = svgViewBox(); + expect(vb).toBeTruthy(); + return vb; + }); 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()); + // The mobile tap recentres the canvas, lifting the anchor above the sheet, + // so the viewBox origin shifts. + await vi.waitFor(() => expect(svgViewBox()).not.toBe(before)); }); it('does not recentre on tap when matchMedia reports desktop (#703 AC8)', async () => { @@ -182,14 +193,17 @@ describe('stammbaum page', () => { render(Stammbaum, { props: { data: { nodes: familyNodes, edges: [], initialView: DEFAULT_VIEW } } }); - await vi.waitFor(() => expect(replaceState).toHaveBeenCalled()); - replaceState.mockClear(); + const before = await vi.waitFor(() => { + const vb = svgViewBox(); + expect(vb).toBeTruthy(); + return vb; + }); 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. + // so the viewBox is unchanged. await expect.element(page.getByRole('complementary')).toBeVisible(); - expect(replaceState).not.toHaveBeenCalled(); + expect(svgViewBox()).toBe(before); }); });