diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index d1fe143..73845cf 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -171,9 +171,19 @@ export function seedDb(): MockDb { }, // UI-POLISH-1: Import-Stub ohne Namen — testet den '(ohne Namen)'-Platzhalter in Liste + Detail. gerbil('nameless-stub', '', 'male', '2023-01-01', null, null), + // SIBLING-PAIRING: Vollgeschwister-Verpaarung (in der Zucht häufig). Beide + // Eltern von 'inzucht-kind' stammen aus DEMSELBEN Wurf 'w-zwillinge' → das + // Diagramm führt ihren Vorfahren-Ast zu einem Verweis-Knoten zusammen. + gerbil('zwilling-bock', 'Zwilling Bock', 'male', '2020-05-01', 'w-zwillinge', 'cv-agouti'), + gerbil('zwilling-maus', 'Zwilling Maus', 'female', '2020-05-01', 'w-zwillinge', 'cv-schwarz'), + { ...gerbil('opa-w', 'Opa W', 'male', '2018-01-01', null, 'cv-agouti'), isResident: false }, + { ...gerbil('oma-u', 'Oma U', 'female', '2018-02-01', null, 'cv-gold'), isResident: false }, + gerbil('inzucht-kind', 'Inzucht Kind', 'female', '2021-06-01', 'w-inzucht', 'cv-agouti'), ] const litters: Litter[] = [ + { id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u' }, + { id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus' }, { id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1 }, { id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja' }, { id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla' }, diff --git a/gerbil-manager-web/e2e/stammbaum.spec.ts b/gerbil-manager-web/e2e/stammbaum.spec.ts index 11e0ad7..982ef67 100644 --- a/gerbil-manager-web/e2e/stammbaum.spec.ts +++ b/gerbil-manager-web/e2e/stammbaum.spec.ts @@ -111,6 +111,33 @@ test('+-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND) await expect(page.getByRole('link', { name: 'Max' })).toBeVisible({ timeout: 8000 }) }) +test('Geschwister-Verpaarung führt den Vorfahren-Ast zu einem Verweis-Knoten zusammen (SIBLING-PAIRING)', async ({ page }) => { + skipUnlessMock() + // 'Inzucht Kind': Eltern Zwilling Bock (♂) + Zwilling Maus (♀) stammen aus + // demselben Wurf 'w-zwillinge' (Opa W × Oma U) → Vollgeschwister. + await page.goto('/rennmaeuse/inzucht-kind/stammbaum') + await expect(page.locator('.pedigree-card').first()).toBeVisible() + + const fit = page.getByRole('button', { name: t.zoomFit }) + if (await fit.isVisible()) await fit.click() + await page.waitForTimeout(600) + + // Vaterlinie zeigt die echten Großeltern (einmal). Der Vater-Knoten trägt den + // Namen als Link (die Verweis-Karte nur als Span — daher Link-Rolle eindeutig). + await expect(page.getByRole('link', { name: 'Zwilling Bock' })).toBeVisible() + await expect(page.locator('.pedigree-card').filter({ hasText: 'Opa W' })).toBeVisible() + await expect(page.locator('.pedigree-card').filter({ hasText: 'Oma U' })).toBeVisible() + + // Mutter-Ast ist zu EINEM Verweis-Knoten zusammengeführt (Verweis auf den Vater). + const ref = page.locator('.pedigree-card--sibling') + await expect(ref).toBeVisible() + await expect(ref).toContainText(t.siblingPairing.refLabel('Zwilling Bock')) + await expect(ref).toContainText(t.siblingPairing.refHint) + + // Mini-Legende ergänzt den Geschwister-Hinweis. + await expect(page.locator('.stammbaum-hints')).toContainText(t.hintSiblings) +}) + test('Würfe-Panel zeigt Würfe des Wurzeltiers + Link öffnet Wurf (STAMMBAUM-LITTERS)', async ({ page }) => { skipUnlessMock() // Fridolin ist Vater von Wurf K (5 Junge) — Panel muss erscheinen. diff --git a/gerbil-manager-web/src/pages/StammbaumPage.tsx b/gerbil-manager-web/src/pages/StammbaumPage.tsx index 21f0547..d52b6d3 100644 --- a/gerbil-manager-web/src/pages/StammbaumPage.tsx +++ b/gerbil-manager-web/src/pages/StammbaumPage.tsx @@ -168,6 +168,13 @@ export default function StammbaumPage() { /* ── react-d3-tree-Daten ── */ const nodesByPath = useMemo(() => (root ? collectNodes(root) : null), [root]) const datum = useMemo(() => (root ? toRawNodeDatum(root, t.unknown) : null), [root, t]) + const hasSiblingPairing = useMemo( + () => + nodesByPath + ? [...nodesByPath.values()].some((n) => n.kind === 'animal' && n.siblingPairing != null) + : false, + [nodesByPath], + ) /* ── Zeichenfläche vermessen (Erst-Zentrierung + Einpassen) ── */ const canvasRef = useRef(null) @@ -246,6 +253,28 @@ export default function StammbaumPage() { const renderNode = useCallback( ({ nodeDatum }: CustomNodeElementProps) => { const path = String(nodeDatum.attributes?.path ?? '') + // Verweis-Knoten der Geschwister-Verpaarung: rein aus den Attributen + // gerendert (kein Eintrag in nodesByPath). + if (nodeDatum.attributes?.kind === 'sibling-ref') { + const refName = String(nodeDatum.attributes.refName ?? '') || de.pages.gerbils.nameless + return ( + + +
+ +
+ + {t.siblingPairing.refLabel(refName)} + + {t.siblingPairing.refHint} +
+
+
+
+ ) + } const node = nodesByPath?.get(path) return ( @@ -353,6 +382,7 @@ export default function StammbaumPage() {
  • {t.tapHint}
  • {t.hintName}
  • {t.hintExpand}
  • + {hasSiblingPairing &&
  • {t.hintSiblings}
  • } @@ -429,7 +459,7 @@ function PedigreeCard({ )} {dob && * {dob}} - {node.expandable && ( + {node.expandable && !node.siblingPairing && (