/** QA-1: Stammbaum (FEAT-4) — interaktiver Viewer, Re-Root, Druckansicht. */ import { de, expect, skipUnlessMock, test } from './fixtures' const t = de.pages.stammbaum test('Viewer rendert die Ahnen-Karten und re-rootet per Tipp aufs Elterntier', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() await expect(page.getByRole('heading', { name: t.titleFor('Krümel') })).toBeVisible() // Erst die Ansicht einpassen, damit alle Karten im SVG-Viewport liegen // (sonst tippt der Klick auf dem Laptop ins Leere). const fit = page.getByRole('button', { name: t.zoomFit }) if (await fit.isVisible()) await fit.click() // Tipp auf den Vater -> Stammbaum wurzelt neu auf Fridolin. // (force: die d3-Baum-Animation bewegt die Karten noch eine Weile — // Playwrights Stabilitäts-Check würde sonst dauerhaft warten.) await page.waitForTimeout(900) await page .locator('.pedigree-card:not(.pedigree-card--root):not(.pedigree-card--unknown)') .filter({ hasText: 'Fridolin' }) .first() .click({ force: true }) await page.waitForURL('**/rennmaeuse/fridolin/stammbaum') await expect(page.getByRole('heading', { name: t.titleFor('Fridolin') })).toBeVisible() }) test('Inzuchtkoeffizient wird angezeigt', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') const inzucht = page.locator('.stammbaum-inzucht') await expect(inzucht).toBeVisible() await expect(inzucht).toContainText(t.inbreeding.label) // 0.0625 -> de-DE mit max. 1 Nachkommastelle -> "6,3 %" await expect(inzucht).toContainText('6,3') }) test('Druckansicht (Ahnentafel) erscheint im Print-Medium', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() await page.emulateMedia({ media: 'print' }) // Spaltenüberschriften der Ahnentafel + Erstellt-am-Zeile await expect(page.getByText(t.printView.generations[1], { exact: true })).toBeVisible() await expect(page.getByText(new RegExp(t.printView.createdOn))).toBeVisible() await page.emulateMedia({ media: 'screen' }) }) test('unbekanntes Tier zeigt den deutschen Nicht-gefunden-Zustand', async ({ page }) => { await page.goto('/rennmaeuse/gibt-es-nicht/stammbaum') await expect(page.getByText(t.notFound).or(page.getByText(de.api.errors.notFound))).toBeVisible() }) test('Ahnen-Namen sind Links zur Tier-Detailseite (STAMMBAUM-OPEN)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() // Fridolin ist Vater von Krümel — sein Name-Link soll /rennmaeuse/fridolin öffnen const nameLink = page.getByRole('link', { name: 'Fridolin' }) await expect(nameLink).toBeVisible() // href korrekt gesetzt await expect(nameLink).toHaveAttribute('href', '/rennmaeuse/fridolin') // Tastatur-Navigation (umgeht Toolbar-Überlappung im SVG-Canvas, testet zugl. Accessibility) await nameLink.focus() await page.keyboard.press('Enter') await expect(page.getByRole('heading', { name: 'Fridolin' })).toBeVisible() await expect(page).toHaveURL(/\/rennmaeuse\/fridolin$/) }) test('Namenloser Ahne zeigt Platzhalter in der Stammbaum-Karte (UI-POLISH-2)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/nameless-stub/stammbaum') // Titel zeigt '(ohne Namen)' await expect(page.getByRole('heading', { name: t.titleFor(de.pages.gerbils.nameless) })).toBeVisible() // Karte selbst zeigt '(ohne Namen)' statt leer await expect(page.locator('.pedigree-card__nametext')).toHaveText(de.pages.gerbils.nameless) }) test('Mini-Legende zeigt alle drei Interaktionshinweise (STAMMBAUM-EXPAND)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() const hints = page.locator('.stammbaum-hints') await expect(hints).toBeVisible() await expect(hints).toContainText(t.tapHint) await expect(hints).toContainText(t.hintName) await expect(hints).toContainText(t.hintExpand) }) test('+-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() // Einpassen, damit die 4. Generation (Emil mit +) im Viewport liegt. const fit = page.getByRole('button', { name: t.zoomFit }) if (await fit.isVisible()) await fit.click() await page.waitForTimeout(600) // +-Button muss an der Tiefengrenze erscheinen (Emil hat litterId w-emil). const expandBtn = page.getByRole('button', { name: t.expand }).first() await expect(expandBtn).toBeVisible() // Klick auf +: Emil wird aufgeklappt → Max (sein Vater) taucht als Link auf. await expandBtn.click({ force: true }) 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. await page.goto('/rennmaeuse/fridolin/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() const panel = page.locator('.stammbaum-litters-panel') await expect(panel).toBeVisible() await expect(panel).toContainText(t.littersTitle) await expect(panel).toContainText('Wurf K') await expect(panel).toContainText('5') // Link-Klick → Wurf-Detailseite const wurfLink = panel.getByRole('link', { name: /Wurf K/ }) await expect(wurfLink).toBeVisible() await wurfLink.click() await expect(page).toHaveURL(/\/wuerfe\/w-kruemel/) }) test('Kein Würfe-Panel wenn Wurzeltier keine Würfe hat (STAMMBAUM-LITTERS)', async ({ page }) => { skipUnlessMock() // Krümel hat noch keine Würfe als Elternteil → Panel muss fehlen. await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() await expect(page.locator('.stammbaum-litters-panel')).not.toBeVisible() }) test('Knoten-Karte zeigt volles Geburtsdatum (STAMMBAUM-FULLDATE)', async ({ page }) => { skipUnlessMock() // Krümel: dateOfBirth '2025-03-12' → formatDate → '12.03.2025' await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() await expect(page.locator('.pedigree-card__year').first()).toContainText('12.03.2025') }) test('Gencode-Chip zeigt Genotyp statt Farbname (STAMMBAUM-GENCODE)', async ({ page }) => { skipUnlessMock() // Fridolin ist Vater von Krümel — genotype 'aa CC DD EE GG PP spsp rere' await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() const fridolinCard = page.locator('.pedigree-card').filter({ hasText: 'Fridolin' }) await expect(fridolinCard.locator('.pedigree-chip')).toContainText('aa CC') }) test('Tier mit Foto zeigt Avatar-Bild, Tier ohne Foto zeigt Rennmaus-Icon (STAMMBAUM-AVATAR)', async ({ page }) => { skipUnlessMock() // Fridolin hat profilePhotoUrl (data URI) → sichtbar // Krümel hat profilePhotoUrl=null → kein img, SVG-Icon sichtbar await page.goto('/rennmaeuse/kruemel/stammbaum') await expect(page.locator('.pedigree-card').first()).toBeVisible() // Fridolin ist Vater von Krümel → seine Karte enthält ein const fridolinCard = page.locator('.pedigree-card').filter({ hasText: 'Fridolin' }) await expect(fridolinCard.locator('img.pedigree-card__avatar')).toBeVisible() // Krümel (root) hat kein Foto → SVG-Icon im Foto-Slot const rootCard = page.locator('.pedigree-card.pedigree-card--root') await expect(rootCard.locator('img.pedigree-card__avatar')).not.toBeAttached() await expect(rootCard.locator('svg[role="img"]')).toBeVisible() })