Files
GerbilManager/gerbil-manager-web/e2e/tiere.spec.ts
Gulum fdad1633cd fix(akte): Komma-Trenner in Listen + Elternnamen in der Wurf-Liste
- Zuchtpartner/Nachzucht: .ak-kv-value ist ein Flex-Container mit wrap, jeder
  Eintrag ein eigenes Item. Das vorangestellte „, " landete beim Umbruch am
  Zeilenanfang („, Zora"). Das Komma klebt jetzt hinten am vorherigen Namen.
  Regressionstest KV-COMMA (schlaegt gegen den alten Code fehl).
- Wurf-Liste/Zuchtpaare: Elternnamen wurden nur aus den ersten 1000 Tieren
  aufgeloest (Prod hat >2400) → Eltern dahinter erschienen als „Unbekannt".
  Jetzt werden alle Tiere geladen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:42:59 +02:00

303 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** QA-1: Tiere (Rennmäuse) — Liste/Suche, CRUD, Detail-Tabs (FEAT-1 + FEAT-6). */
import type { Page } from '@playwright/test'
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')
// Im Tabpanel ansprechen — die Erwerb-Sektion der Akte hat ebenfalls einen „Hinzufügen"-Button.
await page.getByRole('tabpanel').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 zeigt Geschwisterverpaarungs-Hinweis nur wenn die Eltern Vollgeschwister sind (SIBLING-PAIRING)', async ({ page }) => {
skipUnlessMock()
// 'Inzucht Kind': Eltern Zwilling Bock + Zwilling Maus aus demselben Wurf
// 'w-zwillinge' → Vollgeschwister → Hinweis sichtbar.
await page.goto('/rennmaeuse/inzucht-kind')
await expect(page.getByRole('heading', { name: 'Inzucht Kind' })).toBeVisible()
await expect(page.locator('.ak-fact--sibling')).toContainText(t.detail.siblingPairing)
// 'Krümel': Eltern Fridolin × Luna aus verschiedenen Würfen → kein Hinweis.
await page.goto('/rennmaeuse/kruemel')
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
await expect(page.locator('.ak-fact--sibling')).toHaveCount(0)
})
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('Detailseite: externer Vorfahre (isResident=false) blendet Würfe & Charakter aus (#17/#20)', async ({ page }) => {
skipUnlessMock()
// 'hilde' ist isResident=false (externer Ahne, nur für den Stammbaum erfasst).
await page.goto('/rennmaeuse/hilde')
await expect(page.getByRole('heading', { name: 'Hilde' })).toBeVisible()
// Extern-Badge + Hinweis sichtbar, Charakter- und Würfe-Abschnitt ausgeblendet.
await expect(page.getByText(t.detail.nonResidentNote)).toBeVisible()
await expect(page.getByText(de.character.sectionTitle)).toBeHidden()
await expect(page.getByRole('heading', { name: t.detail.parentLittersTitle })).toBeHidden()
// Gegenprobe: Bestandstier Krümel zeigt beide Abschnitte.
await page.goto('/rennmaeuse/kruemel')
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
await expect(page.getByText(de.character.sectionTitle)).toBeVisible()
await expect(page.getByRole('heading', { name: t.detail.parentLittersTitle })).toBeVisible()
await expect(page.getByText(t.detail.nonResidentNote)).toBeHidden()
})
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: Zuchtpartner-Liste trennt mit Komma HINTEN am Namen (KV-COMMA)', async ({
page,
}) => {
skipUnlessMock()
// Anton ist Vater von Wurf B (Mutter Greta) und Wurf C (Mutter Frieda) → 2 Zuchtpartner.
// .ak-kv-value ist ein Flex-Container mit wrap: ein vorangestelltes „, " landete beim
// Umbruch am Zeilenanfang („, Frieda"). Das Komma muss am vorherigen Namen kleben.
await page.goto('/rennmaeuse/anton')
const row = page.locator('.ak-kv', {
has: page.locator('.ak-kv-label', { hasText: de.pages.gerbils.detail.zuchtpartner }),
})
await expect(row).toBeVisible()
const value = row.locator('.ak-kv-value')
const text = (await value.innerText()).replace(/\s+/g, ' ').trim()
expect(text).not.toMatch(/^,/)
expect(text).toBe('Greta, Frieda')
})
/** QOL-ELTERN: Vater/Mutter im Abstammungs-Block des Tier-Formulars setzen. */
async function pickParent(page: Page, legend: string, name: string) {
const group = page.getByRole('group', { name: legend })
await group.getByRole('searchbox').fill(name)
await group.getByRole('button', { name, exact: true }).click()
}
test('Tier bearbeiten — Eltern direkt an der Akte setzen, Wurf wird angelegt (QOL-ELTERN)', async ({
page,
}) => {
skipUnlessMock()
// Maja hat keinen Geburtswurf → beim Speichern wird einer für die Eltern angelegt.
await page.goto('/rennmaeuse/maja/bearbeiten')
await expect(page.getByRole('heading', { name: t.form.editTitle })).toBeVisible()
await expect(page.getByText(t.form.parentsSectionTitle)).toBeVisible()
await pickParent(page, t.detail.parentLittersRoleVater, 'Anton')
await pickParent(page, t.detail.parentLittersRoleMutter, 'Greta')
// Hinweis, dass ein Wurf-Eintrag angelegt wird (kein Wurf vorhanden).
await expect(page.getByTestId('parents-new-litter-hint')).toBeVisible()
await page.getByRole('button', { name: t.form.save, exact: true }).click()
// Akte zeigt die neuen Eltern als Links.
await expect(page.getByRole('heading', { name: 'Maja' })).toBeVisible()
await expect(page.getByRole('link', { name: 'Anton', exact: true })).toBeVisible()
await expect(page.getByRole('link', { name: 'Greta', exact: true })).toBeVisible()
})
test('Tier bearbeiten — Eltern-Wechsel warnt vor den Geschwistern desselben Wurfs (QOL-ELTERN)', async ({
page,
}) => {
skipUnlessMock()
// Krümel hängt an Wurf K (Vater Fridolin, Mutter Luna) mit 2 weiteren Jungtieren.
await page.goto('/rennmaeuse/kruemel/bearbeiten')
const vater = page.getByRole('group', { name: t.detail.parentLittersRoleVater })
await expect(vater.getByText('Fridolin')).toBeVisible()
// Vater tauschen → Warnung, dass die Änderung auch für die Geschwister gilt.
await vater.getByRole('button', { name: t.form.parentsClear }).click()
await pickParent(page, t.detail.parentLittersRoleVater, 'Anton')
await expect(page.getByTestId('parents-sibling-warning')).toContainText('2')
await page.getByRole('button', { name: t.form.save, exact: true }).click()
await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible()
await expect(page.getByRole('link', { name: 'Anton', exact: true })).toBeVisible()
})
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 — gezielt IM Abschnitt „Würfe als Elternteil": der Abschnitt
// „Nachzucht in der Zucht" nennt den Wurfnamen ebenfalls (Krümel-Karte) und würde die
// Suche über die ganze Seite mehrdeutig machen.
const littersSection = page.locator('section.ak-card', {
has: page.getByRole('heading', { name: t.detail.parentLittersTitle }),
})
const litterLink = littersSection.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(
littersSection.locator('.badge', { hasText: t.detail.parentLittersRoleVater }).first(),
).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()
})