- 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>
303 lines
15 KiB
TypeScript
303 lines
15 KiB
TypeScript
/** 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()
|
||
})
|