/** QA-1: Tiere (Rennmäuse) — Liste/Suche, CRUD, Detail-Tabs (FEAT-1 + FEAT-6). */ import { de, expect, gotoSection, selectListFilter, skipUnlessMock, test, uniqueName } from './fixtures' const t = de.pages.gerbils const tabs = de.pages.tierTabs test('Liste lädt und die Namenssuche filtert', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse') await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible() await expect(page.getByRole('link', { name: /Fridolin/ })).toBeVisible() await page.getByPlaceholder(t.searchPlaceholder).first().fill('Krüm') await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible() await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden() }) test('Herkunft-Filter (originBreeder) zeigt nur Tiere der gewählten Zucht (SEARCH-2b)', async ({ page, }) => { skipUnlessMock() await page.goto('/rennmaeuse') await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible() await expect(page.getByRole('link', { name: /Fridolin/ })).toBeVisible() // DESIGN (Rennmaus Filter): Herkunft (originBreeder) über das Quick-Chips-Sheet // auf die Seed-Zucht 'Clan-Kleine-Chaoten' (nur Krümel) filtern. await selectListFilter(page, t.fields.origin, 'Clan-Kleine-Chaoten') await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible() await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden() }) test('Namenssuche ist trennzeichen-unempfindlich (SEARCH-2b)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse') // 'Krü mel' (mit Leerzeichen) wird normalisiert zu 'krümel' -> matcht Krümel. await page.getByPlaceholder(t.searchPlaceholder).first().fill('Krü mel') await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible() await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden() }) test('Neue Rennmaus anlegen — Validierung + Erfolg', async ({ page }) => { await gotoSection(page, de.nav.gerbils) await page.getByRole('link', { name: t.newButton }).click() await expect(page.getByRole('heading', { name: t.form.createTitle })).toBeVisible() // Pflichtfeld-Validierung auf Deutsch await page.getByRole('button', { name: t.form.save, exact: true }).click() await expect(page.getByText(t.validation.nameRequired)).toBeVisible() await expect(page.getByText(t.validation.genderRequired)).toBeVisible() const name = uniqueName('Testmaus') await page.getByLabel(`${t.fields.name} *`).fill(name) await page.getByRole('button', { name: t.genderLabels.female, exact: false }).click() await page.getByRole('button', { name: t.form.save, exact: true }).click() // Erfolg: Detailseite des neuen Tieres await expect(page.getByRole('heading', { name })).toBeVisible() await expect(page.getByText(t.detail.masterData)).toBeVisible() }) test('Detailseite zeigt Stammdaten + Tab-Inhalte (Gesundheit/Gewicht/Fotos)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel') await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible() await expect(page.getByText(t.detail.masterData)).toBeVisible() // Fotos (Standard-Tab): leerer Zustand + Datei-Pflicht-Validierung await expect(page.getByText(tabs.photos.empty)).toBeVisible() await page.getByRole('button', { name: tabs.photos.uploadButton }).click() await expect(page.getByText(tabs.photos.validation.fileRequired)).toBeVisible() // Gesundheit: Seed-Eintrag sichtbar, neuen Eintrag anlegen await page.getByRole('tab', { name: t.detail.tabs.health }).click() await expect(page.getByText('Jahresimpfung')).toBeVisible() await expect(page.getByText(tabs.health.typeLabels.Vaccination, { exact: true })).toBeVisible() await page.getByRole('button', { name: tabs.health.newButton }).click() // 'Art' exakt ansteuern ('Tierarzt' enthält 'arzt'; Label-Text enthält die Optionen) await page .locator('label.field', { has: page.locator(`span:text-is("${tabs.health.fields.type}")`) }) .locator('select') .selectOption({ label: tabs.health.typeLabels.Examination }) await page.getByLabel(`${tabs.health.fields.description} *`).fill('Routinekontrolle, alles gut') await page.getByRole('button', { name: tabs.actions.save, exact: true }).click() await expect(page.getByText('Routinekontrolle, alles gut')).toBeVisible() // Gewicht: Verlaufskurve (2 Seed-Einträge) + Schnell-Erfassung await page.getByRole('tab', { name: t.detail.tabs.weight }).click() await expect(page.locator('svg.line-chart')).toBeVisible() await expect(page.getByText(/78\s*g/)).toBeVisible() await page.getByLabel(tabs.weight.fields.weightGrams).fill('85') await page.getByRole('button', { name: tabs.weight.addButton, exact: true }).click() await expect(page.getByText(/85\s*g/)).toBeVisible() }) test('Detailseite: Wurf-Feld ist ein Link zur Wurf-Detailseite (WURF-LINK)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel') // Krümel hat litterId 'w-kruemel' → Name 'Wurf K' → Link /wuerfe/w-kruemel const litterLink = page.getByRole('link', { name: 'Wurf K' }) await expect(litterLink).toBeVisible() await litterLink.click() await expect(page.getByRole('heading', { name: 'Wurf K' })).toBeVisible() }) test('Detailseite: Herkunft zeigt originBreeder als Text wenn kein Kontakt (WURF-LINK-Addendum)', async ({ page }) => { skipUnlessMock() // Fridolin hat originContactId=null + originBreeder='Zoohandlung Meier' → plain text, kein Link await page.goto('/rennmaeuse/fridolin') await expect(page.getByRole('heading', { name: 'Fridolin' })).toBeVisible() await expect(page.getByText('Zoohandlung Meier')).toBeVisible() // Kein Link mit diesem Namen — es ist reiner Text await expect(page.getByRole('link', { name: 'Zoohandlung Meier' })).toBeHidden() }) test('Detailseite: Herkunft zeigt Kontaktlink wenn originContactId gesetzt (WURF-LINK-Addendum)', async ({ page }) => { skipUnlessMock() // Krümel hat originContactId='con-meier' → Link /kontakte/con-meier (Priorität vor originBreeder) await page.goto('/rennmaeuse/kruemel') const originLink = page.getByRole('link', { name: 'Zoohandlung Meier' }) await expect(originLink).toBeVisible() await expect(originLink).toHaveAttribute('href', '/kontakte/con-meier') }) test('Namenlose Tiere zeigen Platzhalter in der Liste (UI-POLISH-1)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse') // 'nameless-stub' hat name='' → Platzhalter '(ohne Namen)' soll als Link sichtbar sein await expect(page.getByRole('link', { name: de.pages.gerbils.nameless })).toBeVisible() }) test('Namenlose Tiere zeigen Platzhalter im Detail-Header (UI-POLISH-1)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/nameless-stub') await expect(page.getByRole('heading', { name: de.pages.gerbils.nameless })).toBeVisible() }) test('Tier bearbeiten — Notizen ändern', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/bearbeiten') await expect(page.getByRole('heading', { name: t.form.editTitle })).toBeVisible() await page.getByLabel(t.fields.notes).fill('Sehr zutraulich.') await page.getByRole('button', { name: t.form.save, exact: true }).click() await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible() await expect(page.getByText('Sehr zutraulich.')).toBeVisible() }) test('Tier bearbeiten — originBreeder Freitext round-trip (FORM-FIELDS-1)', async ({ page }) => { skipUnlessMock() // Fridolin hat originBreeder='Zoohandlung Meier', kein originContactId → Freitext-Feld vorbelegt await page.goto('/rennmaeuse/fridolin/bearbeiten') await expect(page.getByLabel(t.fields.originBreeder)).toHaveValue('Zoohandlung Meier') await page.getByLabel(t.fields.originBreeder).fill('Neue Musterzucht') await page.getByRole('button', { name: t.form.save, exact: true }).click() // Detailseite: Herkunft zeigt den neuen Wert (kein Contact → originBreeder-Text) await expect(page.getByText('Neue Musterzucht')).toBeVisible() }) test('Tier bearbeiten — isDeaf Tristate round-trip (FORM-FIELDS-1)', async ({ page }) => { skipUnlessMock() await page.goto('/rennmaeuse/kruemel/bearbeiten') // Standard: Unbekannt (null → value='') await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('') await page.getByLabel(t.fields.isDeaf).selectOption({ label: t.form.isDeafYes }) await page.getByRole('button', { name: t.form.save, exact: true }).click() // Zurück zur Bearbeiten-Seite: Wert muss erhalten sein await page.goto('/rennmaeuse/kruemel/bearbeiten') await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true') }) test('Detailseite: Würfe als Elternteil zeigt verlinkten Wurf (ANIMAL-LITTERS)', async ({ page }) => { skipUnlessMock() // Fridolin ist fatherId von 'w-kruemel' (Wurf K) await page.goto('/rennmaeuse/fridolin') await expect(page.getByRole('heading', { name: 'Fridolin' })).toBeVisible() await expect(page.getByRole('heading', { name: t.detail.parentLittersTitle })).toBeVisible() // Wurf K als Link sichtbar const litterLink = page.getByRole('link', { name: /Wurf K/ }) await expect(litterLink).toBeVisible() // Vater-Rollen-Badge im Wurf-Eintrag sichtbar (die Akte zeigt „Vater" jetzt auch // als Stammdaten-Zeile, daher gezielt auf das Badge eingrenzen). await expect(page.locator('.badge', { hasText: t.detail.parentLittersRoleVater })).toBeVisible() // Navigation zum Wurf funktioniert await litterLink.first().click() await expect(page.getByRole('heading', { name: 'Wurf K' })).toBeVisible() }) test('Detailseite: Keine Würfe als Elternteil zeigt Leer-Zustand (ANIMAL-LITTERS)', async ({ page }) => { skipUnlessMock() // Krümel hat keine Würfe als Elternteil (nur als Kind von w-kruemel) await page.goto('/rennmaeuse/kruemel') await expect(page.getByRole('heading', { name: t.detail.parentLittersTitle })).toBeVisible() await expect(page.getByText(t.detail.parentLittersEmpty)).toBeVisible() })