feat(tiere): Eltern direkt in der Tier-Akte bearbeiten (QOL)
All checks were successful
CI / Backend Tests (.NET) (push) Successful in 1m15s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m37s
CI / Docker Build & Push (push) Successful in 7m36s
CI / Deploy auf TrueNAS (Custom App) (push) Successful in 8s

Im Tier-Formular gibt es jetzt den Block „Abstammung" mit Wurf-Auswahl und
Vater-/Mutter-Picker — die Eltern müssen nicht mehr über die Wurf-Seite
gesucht und dort editiert werden. Das Datenmodell bleibt unverändert: Eltern
hängen weiterhin am Geburtswurf.

- Backend: PUT /gerbils/{id}/parents schreibt Litter.FatherId/MotherId des
  Geburtswurfs. Ohne Wurf wird ein bestehender mit gleichem Elternpaar +
  gleichem Datum verknüpft, sonst ein Träger-Wurf angelegt
  ("Wurf von X + Y", ShowInChronicle=false, IsManual=true).
  Geschlechts-Regel wiederverwendet LitterEndpoints.ValidateParents,
  Selbstbezug (Tier als eigener Elternteil) wird abgewiesen.
- UI: Vorbelegung aus dem gewählten Wurf, Warnung mit Anzahl der Geschwister
  (Eltern gehören dem Wurf → Änderung gilt für alle), Hinweis wenn ein
  Wurf-Eintrag angelegt wird. Texte in de.ts.
- Nebenbei: Speichern nutzt im Edit-Modus die Route-Id (PUT /gerbils/{id}
  antwortet 204 ohne Body) und der vorher schon rote Spec-Locator
  „Würfe als Elternteil" ist auf den Abschnitt eingegrenzt.
- Tests: GerbilParentsTests (9), e2e tiere.spec (2 neu) + Mock-Route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 20:43:28 +02:00
parent 7a2feac04d
commit 54e873c115
11 changed files with 653 additions and 29 deletions

View File

@@ -1,4 +1,5 @@
/** 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
@@ -202,18 +203,73 @@ test('Tier bearbeiten — isDeaf Tristate round-trip (FORM-FIELDS-1)', async ({
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true')
})
/** 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
const litterLink = page.getByRole('link', { name: /Wurf K/ })
// 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(page.locator('.badge', { hasText: t.detail.parentLittersRoleVater })).toBeVisible()
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()