fix(stammbaum): make gutter visibility prop-overridable for tests (#689)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m45s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m54s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
CI / Unit & Component Tests (push) Successful in 3m49s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 4m14s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m3s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m45s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m54s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
CI / Unit & Component Tests (push) Successful in 3m49s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 4m14s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m3s
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 <noreply@anthropic.com>
This commit was merged in pull request #690.
This commit is contained in:
@@ -18,9 +18,16 @@ interface Props {
|
|||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
onSelect: (id: string) => void;
|
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<Layout>(() => buildLayout(nodes, edges));
|
const layout = $derived.by<Layout>(() => buildLayout(nodes, edges));
|
||||||
|
|
||||||
@@ -39,13 +46,15 @@ let isMdOrUp = $state(
|
|||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
if (showGutter !== undefined) return;
|
||||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||||
const mq = window.matchMedia(GUTTER_MEDIA_QUERY);
|
const mq = window.matchMedia(GUTTER_MEDIA_QUERY);
|
||||||
const handler = (e: MediaQueryListEvent) => (isMdOrUp = e.matches);
|
const handler = (e: MediaQueryListEvent) => (isMdOrUp = e.matches);
|
||||||
mq.addEventListener('change', handler);
|
mq.addEventListener('change', handler);
|
||||||
return () => mq.removeEventListener('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 };
|
type GutterRow = { rank: number; y: number; label: number | null };
|
||||||
const gutterRows = $derived.by<GutterRow[]>(() => {
|
const gutterRows = $derived.by<GutterRow[]>(() => {
|
||||||
|
|||||||
@@ -651,6 +651,8 @@ describe('StammbaumTree node rendering branches', () => {
|
|||||||
|
|
||||||
describe('StammbaumTree generation gutter (#689)', () => {
|
describe('StammbaumTree generation gutter (#689)', () => {
|
||||||
it('renders a G{n} label per occupied generation row when at least one node carries generation', async () => {
|
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, {
|
render(StammbaumTree, {
|
||||||
nodes: [
|
nodes: [
|
||||||
{ id: ID_A, displayName: 'Walter', familyMember: true, generation: 2 },
|
{ id: ID_A, displayName: 'Walter', familyMember: true, generation: 2 },
|
||||||
@@ -659,7 +661,8 @@ describe('StammbaumTree generation gutter (#689)', () => {
|
|||||||
edges: [],
|
edges: [],
|
||||||
selectedId: null,
|
selectedId: null,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
onSelect: () => {}
|
onSelect: () => {},
|
||||||
|
showGutter: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const labels = Array.from(document.querySelectorAll('g[role="text"]')).map((g) =>
|
const labels = Array.from(document.querySelectorAll('g[role="text"]')).map((g) =>
|
||||||
@@ -675,7 +678,8 @@ describe('StammbaumTree generation gutter (#689)', () => {
|
|||||||
edges: [],
|
edges: [],
|
||||||
selectedId: null,
|
selectedId: null,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
onSelect: () => {}
|
onSelect: () => {},
|
||||||
|
showGutter: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const g3 = Array.from(document.querySelectorAll('g[role="text"]')).find(
|
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/);
|
expect(g3!.querySelector('text')!.textContent).toMatch(/G\s*3/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('omits the gutter when matchMedia (min-width: 768px) is false', async () => {
|
it('omits the gutter when showGutter is false (mobile breakpoint case)', async () => {
|
||||||
const originalMatchMedia = window.matchMedia;
|
render(StammbaumTree, {
|
||||||
window.matchMedia = ((query: string) => ({
|
nodes: [{ id: ID_A, displayName: 'Herbert', familyMember: true, generation: 3 }],
|
||||||
matches: false,
|
edges: [],
|
||||||
media: query,
|
selectedId: null,
|
||||||
onchange: null,
|
zoom: 1,
|
||||||
addEventListener: () => {},
|
onSelect: () => {},
|
||||||
removeEventListener: () => {},
|
showGutter: false
|
||||||
addListener: () => {},
|
});
|
||||||
removeListener: () => {},
|
|
||||||
dispatchEvent: () => false
|
|
||||||
})) as unknown as typeof window.matchMedia;
|
|
||||||
|
|
||||||
try {
|
const labelGroups = Array.from(document.querySelectorAll('g[role="text"]'));
|
||||||
render(StammbaumTree, {
|
expect(labelGroups).toHaveLength(0);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user