From 4e8a430dc32b65182ba1dfda09e9869d96a8cc7a Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 4 Jun 2026 14:16:01 +0200 Subject: [PATCH] fix(stammbaum): raise cross-link opacity to 0.7 + add dash-render test (#724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups: - Leonie/UX: 0.55 navy on the sand canvas was ~2.6:1, under the WCAG 1.4.11 3:1 non-text floor for senior readers; 0.7 clears it. - Sara/QA: add a browser test that actually renders a cross-level link and asserts the distinct 2 6 dash, and that a non-cross-link parent edge stays solid — the cadence was previously only validated via the structural crossLinks array, never where it renders. Co-Authored-By: Claude Opus 4.8 --- .../genealogy/StammbaumConnectors.svelte | 6 +- .../StammbaumConnectors.svelte.test.ts | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/person/genealogy/StammbaumConnectors.svelte.test.ts diff --git a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte index 4e0c1b66..d60c85fd 100644 --- a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte @@ -28,9 +28,11 @@ let { edges, positions, crossLinks = [], isConnectorActive = () => true }: Props // Dash cadence + opacity for a cross-link, deliberately distinct from the // ended-marriage spouse dash (`4 4`) so the two never read alike (WCAG 1.4.1: -// geometry carries the meaning too, not stroke alone). +// geometry carries the meaning too, not stroke alone). Opacity stays at 0.7 so +// the dotted line clears the WCAG 1.4.11 3:1 non-text contrast floor for +// senior / low-vision readers (a lighter 0.55 fell just under). const CROSS_LINK_DASH = '2 6'; -const CROSS_LINK_OPACITY = 0.55; +const CROSS_LINK_OPACITY = 0.7; const crossLinkSet = $derived(new SvelteSet(crossLinks.map((c) => `${c.parentId}->${c.childId}`))); function isCrossLink(parentId: string, childId: string): boolean { diff --git a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte.test.ts b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte.test.ts new file mode 100644 index 00000000..0d9d34db --- /dev/null +++ b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import StammbaumConnectors from './StammbaumConnectors.svelte'; +import { NODE_H } from './layout/buildLayout'; +import type { components } from '$lib/generated/api'; + +type RelationshipDTO = components['schemas']['RelationshipDTO']; + +const P = '00000000-0000-0000-0000-0000000000c1'; +const C = '00000000-0000-0000-0000-0000000000c2'; +const H = '00000000-0000-0000-0000-0000000000c3'; +const W = '00000000-0000-0000-0000-0000000000c4'; + +function parentEdge(parentId: string, childId: string): RelationshipDTO { + return { + id: `${parentId}>${childId}`, + personId: parentId, + relatedPersonId: childId, + personDisplayName: '', + relatedPersonDisplayName: '', + relationType: 'PARENT_OF' + }; +} + +function endedSpouseEdge(a: string, b: string): RelationshipDTO { + return { + id: `${a}~${b}`, + personId: a, + relatedPersonId: b, + personDisplayName: '', + relatedPersonDisplayName: '', + relationType: 'SPOUSE_OF', + toYear: 1950 + }; +} + +const positions = new Map([ + [P, { x: 0, y: 0 }], + [C, { x: 400, y: NODE_H + 80 }], + [H, { x: 0, y: 400 }], + [W, { x: 300, y: 400 }] +]); + +const dashesOf = (selector: string) => + Array.from(document.querySelectorAll(selector)) + .map((el) => el.getAttribute('stroke-dasharray')) + .filter((d): d is string => d !== null); + +describe('StammbaumConnectors — cross-link cadence (#724)', () => { + it('renders a cross-level link with the distinct 2 6 dash, never the 4 4 ended-marriage dash', async () => { + render(StammbaumConnectors, { + edges: [parentEdge(P, C), endedSpouseEdge(H, W)], + positions, + crossLinks: [{ parentId: P, childId: C }] + }); + + await vi.waitFor(() => { + const dashes = dashesOf('line'); + // The cross-link cadence is present … + expect(dashes).toContain('2 6'); + // … the ended-marriage cadence is present … + expect(dashes).toContain('4 4'); + // … and the two are genuinely different cadences (WCAG 1.4.1: not by + // stroke alone, but they must not collapse into the same pattern). + expect('2 6').not.toBe('4 4'); + }); + }); + + it('draws a normal parent→child connector solid when it is NOT a cross-link', async () => { + render(StammbaumConnectors, { + edges: [parentEdge(P, C)], + positions, + crossLinks: [] + }); + + await vi.waitFor(() => { + // No dashed parent lines at all when nothing is a cross-link. + expect(dashesOf('line')).not.toContain('2 6'); + expect(document.querySelectorAll('line').length).toBeGreaterThan(0); + }); + }); +});