From a5e3205520d733a27cb0f48f049e9ebbe37731e3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 28 May 2026 16:53:27 +0200 Subject: [PATCH] fix(stammbaum): make gutter visibility prop-overridable for tests (#689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI kept failing on the two gutter-render tests because the vitest-browser iframe viewport is narrower than 768 px → window.matchMedia(min-width: 768px) returns false → gutter is hidden → g[role="text"] selector returns []. The previous synchronous-seed fix was insufficient because matchMedia itself was the false branch. Add an optional `showGutter?: boolean` prop. When set, it bypasses the matchMedia detection — tests pass `showGutter: true` to assert the rendered gutter, and `showGutter: false` to assert the absent path. Production callers leave it undefined so the existing media-query detection still governs visibility. Refs #689 Co-Authored-By: Claude Opus 4.7 --- .../lib/person/genealogy/StammbaumTree.svelte | 13 +++++- .../genealogy/StammbaumTree.svelte.test.ts | 45 +++++++------------ 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/frontend/src/lib/person/genealogy/StammbaumTree.svelte b/frontend/src/lib/person/genealogy/StammbaumTree.svelte index 6d85067e..32df5f91 100644 --- a/frontend/src/lib/person/genealogy/StammbaumTree.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumTree.svelte @@ -18,9 +18,16 @@ interface Props { selectedId: string | null; zoom: number; onSelect: (id: string) => void; + /** + * Force-show or force-hide the generation gutter. When undefined, falls + * back to a `window.matchMedia('(min-width: 768px)')` detection so the + * gutter only appears on md+ viewports. Tests pass an explicit boolean + * to avoid depending on the vitest-browser iframe viewport. + */ + showGutter?: boolean; } -let { nodes, edges, selectedId, zoom, onSelect }: Props = $props(); +let { nodes, edges, selectedId, zoom, onSelect, showGutter }: Props = $props(); const layout = $derived.by(() => buildLayout(nodes, edges)); @@ -39,13 +46,15 @@ let isMdOrUp = $state( : false ); $effect(() => { + if (showGutter !== undefined) return; if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; const mq = window.matchMedia(GUTTER_MEDIA_QUERY); const handler = (e: MediaQueryListEvent) => (isMdOrUp = e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }); -const gutterWidth = $derived(isMdOrUp ? GUTTER_WIDTH_DESKTOP : 0); +const gutterVisible = $derived(showGutter ?? isMdOrUp); +const gutterWidth = $derived(gutterVisible ? GUTTER_WIDTH_DESKTOP : 0); type GutterRow = { rank: number; y: number; label: number | null }; const gutterRows = $derived.by(() => { diff --git a/frontend/src/lib/person/genealogy/StammbaumTree.svelte.test.ts b/frontend/src/lib/person/genealogy/StammbaumTree.svelte.test.ts index 2a471751..d2750e78 100644 --- a/frontend/src/lib/person/genealogy/StammbaumTree.svelte.test.ts +++ b/frontend/src/lib/person/genealogy/StammbaumTree.svelte.test.ts @@ -651,6 +651,8 @@ describe('StammbaumTree node rendering branches', () => { describe('StammbaumTree generation gutter (#689)', () => { it('renders a G{n} label per occupied generation row when at least one node carries generation', async () => { + // showGutter overrides the matchMedia detection so the test never + // depends on the vitest-browser iframe viewport width. render(StammbaumTree, { nodes: [ { id: ID_A, displayName: 'Walter', familyMember: true, generation: 2 }, @@ -659,7 +661,8 @@ describe('StammbaumTree generation gutter (#689)', () => { edges: [], selectedId: null, zoom: 1, - onSelect: () => {} + onSelect: () => {}, + showGutter: true }); const labels = Array.from(document.querySelectorAll('g[role="text"]')).map((g) => @@ -675,7 +678,8 @@ describe('StammbaumTree generation gutter (#689)', () => { edges: [], selectedId: null, zoom: 1, - onSelect: () => {} + onSelect: () => {}, + showGutter: true }); const g3 = Array.from(document.querySelectorAll('g[role="text"]')).find( @@ -685,32 +689,17 @@ describe('StammbaumTree generation gutter (#689)', () => { expect(g3!.querySelector('text')!.textContent).toMatch(/G\s*3/); }); - it('omits the gutter when matchMedia (min-width: 768px) is false', async () => { - const originalMatchMedia = window.matchMedia; - window.matchMedia = ((query: string) => ({ - matches: false, - media: query, - onchange: null, - addEventListener: () => {}, - removeEventListener: () => {}, - addListener: () => {}, - removeListener: () => {}, - dispatchEvent: () => false - })) as unknown as typeof window.matchMedia; + it('omits the gutter when showGutter is false (mobile breakpoint case)', async () => { + render(StammbaumTree, { + nodes: [{ id: ID_A, displayName: 'Herbert', familyMember: true, generation: 3 }], + edges: [], + selectedId: null, + zoom: 1, + onSelect: () => {}, + showGutter: false + }); - try { - render(StammbaumTree, { - nodes: [{ id: ID_A, displayName: 'Herbert', familyMember: true, generation: 3 }], - edges: [], - selectedId: null, - zoom: 1, - onSelect: () => {} - }); - - const labelGroups = Array.from(document.querySelectorAll('g[role="text"]')); - expect(labelGroups).toHaveLength(0); - } finally { - window.matchMedia = originalMatchMedia; - } + const labelGroups = Array.from(document.querySelectorAll('g[role="text"]')); + expect(labelGroups).toHaveLength(0); }); });