/** * QA-SMOKE-2: Gezielter Live-Smoke der heute gemergten Änderungen (main e6b513e). * Läuft NUR im LIVE-Modus (E2E_BASE_URL gesetzt). * * Abgedeckte Änderungen: * 1. GEN-3h — Bracket-Notation: eef→ee[f], cchm→c[chm], C/cchm→Cc[chm] * 2. FORM-FIELDS — originBreeder + isDeaf im Tier-Editformular * 3. CR-2 — Partial-Update nullt keine bestehenden Felder mehr * 4. UI-POLISH — '(ohne Namen)'-Platzhalter für namenlose Tiere * 5. GEHEGE — Redirects + Nav (bereits in gehege-regression.spec; hier live-Verifikation) * * Read-Only soweit möglich. CR-2-Test: idempotentes Save (keine Wertänderung). */ import { de, expect, skipUnlessLive, test } from './fixtures' const API = 'http://localhost:5179' // ── 1. GEN-3h Bracket-Notation ─────────────────────────────────────────────── test('GEN-3h: eef zeigt ee[f] (nicht "eef") im Tier-Detail', async ({ page }) => { skipUnlessLive() // Roswitha — genotype: "aa CC D? eef Gg P? spsp ??" // eef = e/ef het → E>e>ef Rank → ee[f] await page.goto('/rennmaeuse/66ee246c-26f0-41ac-8b6c-c151d9287633') await expect(page.getByRole('heading', { name: 'Roswitha von den Kleinen Chaoten' })).toBeVisible() // Genotyp-Block soll e[f] enthalten, nicht rohes 'eef' const genoCode = page.locator('code').filter({ hasText: /e\[f\]/ }) await expect(genoCode).toBeVisible({ timeout: 8_000 }) // Rohes 'eef' darf nicht mehr im Code-Block stehen await expect(page.locator('code').filter({ hasText: /[^[]eef/ })).toBeHidden() }) test('GEN-3h: cchm/cchm zeigt c[chm]c[chm] im Tier-Detail (ZoneFire)', async ({ page }) => { skipUnlessLive() // ZoneFire — genotype: "Aa cchmcchm D? Ee Gg P? Spsp ??" await page.goto('/rennmaeuse/14d5cea9-0f87-4381-8afe-80f1d58f7b76') await expect(page.getByRole('heading', { name: 'ZoneFire' })).toBeVisible() const genoCode = page.locator('code').filter({ hasText: /c\[chm\]/ }) await expect(genoCode).toBeVisible({ timeout: 8_000 }) await expect(page.locator('code').filter({ hasText: /cchmcchm/ })).toBeHidden() }) test('GEN-3h: Cc[chm] + ee[f] korrekt bei Ethan (gemischter Locus)', async ({ page }) => { skipUnlessLive() // Ethan — genotype: "Aa Ccchm D? eef Gg Pp Spsp ??" // Ccchm → Cc[chm]; eef → ee[f] await page.goto('/rennmaeuse/54ef9b08-0def-496e-95db-5107b732ddd4') await expect(page.getByRole('heading', { name: 'Ethan von den Kleinen Chaoten' })).toBeVisible() const genoCode = page.locator('code') await expect(genoCode.filter({ hasText: /c\[chm\]/ })).toBeVisible({ timeout: 8_000 }) await expect(genoCode.filter({ hasText: /e\[f\]/ })).toBeVisible() // Rohes Storage-Format darf nicht sichtbar sein await expect(genoCode.filter({ hasText: /Ccchm|eef/ })).toBeHidden() }) // ── 2. FORM-FIELDS: originBreeder + isDeaf im Formular ─────────────────────── test('FORM-FIELDS: Herkunft-Freitext + Gehörlos-Dropdown im Tier-Editformular', async ({ page, }) => { skipUnlessLive() // Ethan hat bereits originBreeder="Kleinen Chaoten", isDeaf=null await page.goto('/rennmaeuse/54ef9b08-0def-496e-95db-5107b732ddd4/bearbeiten') await expect(page.getByRole('heading', { name: de.pages.gerbils.form.editTitle })).toBeVisible({ timeout: 8_000 }) // Herkunft-Freitext (originBreeder) soll sichtbar und gefüllt sein const originInput = page.getByLabel(de.pages.gerbils.fields.originBreeder) await expect(originInput).toBeVisible() await expect(originInput).toHaveValue('Kleinen Chaoten') // Gehörlos-Dropdown (isDeaf) soll sichtbar sein mit "Unbekannt"-Default // Gehörlos-Dropdown (isDeaf): label 'Gehörlos', aktuell leer (Unbekannt) const deafLabel = page.locator('label').filter({ hasText: de.pages.gerbils.fields.isDeaf }) await expect(deafLabel).toBeVisible() // Dropdown-Select im Label-Kontext const deafSelect = deafLabel.locator('select') await expect(deafSelect).toBeVisible() }) // ── 3. CR-2: idempotentes Save nullt keine Felder ──────────────────────────── test('CR-2: Partial-PUT nullt keine bestehenden Felder (direkter API-Test)', async ({ page }) => { skipUnlessLive() // Snapshot VORHER const before = await page.request.get(`${API}/gerbils/54ef9b08-0def-496e-95db-5107b732ddd4`) const snap = await before.json() expect(snap.originBreeder).toBe('Kleinen Chaoten') expect(snap.genotype).toBeTruthy() // Partial-PUT: nur name + gender senden (minimal body, alle anderen Felder fehlen) // Mit PATCH-Semantik-Fix: fehlende Felder werden NICHT genullt const res = await page.request.put(`${API}/gerbils/54ef9b08-0def-496e-95db-5107b732ddd4`, { data: { name: snap.name, gender: snap.gender, status: snap.status, isResident: snap.isResident, // ALLE anderen Felder absichtlich weggelassen → vor CR-2-Fix würden sie auf null gesetzt }, }) // PATCH-Semantik: 204 No Content (kein Body), fehlende Felder bleiben erhalten expect(res.status()).toBe(204) // Re-Fetch und vergleichen const afterRes = await page.request.get(`${API}/gerbils/54ef9b08-0def-496e-95db-5107b732ddd4`) const updated = await afterRes.json() expect(updated.originBreeder).toBe(snap.originBreeder) // muss erhalten bleiben expect(updated.genotype).toBe(snap.genotype) // muss erhalten bleiben expect(updated.dateOfBirth).toBe(snap.dateOfBirth) // muss erhalten bleiben expect(updated.litterId).toBe(snap.litterId) // muss erhalten bleiben expect(updated.externalRef).toBe(snap.externalRef) // muss erhalten bleiben // Restore: idempotent (gleiche Werte gesetzt, kein Net-Change) }) // ── 4. UI-POLISH: '(ohne Namen)'-Platzhalter ───────────────────────────────── test('UI-POLISH: namenlose Tiere zeigen "(ohne Namen)" in der Tierliste', async ({ page }) => { skipUnlessLive() // Alle Tiere anzeigen (inkl. externe), um namenlose zu sehen await page.goto('/rennmaeuse') await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible() // "(ohne Namen)" Platzhalter soll mindestens einmal sichtbar sein // (Filter auf alle Tiere — ggf. "Externe anzeigen" aktivieren) const placeholder = page.getByText('(ohne Namen)') // Wenn nicht sofort sichtbar, Toggle "Externe anzeigen" probieren if (!(await placeholder.isVisible({ timeout: 3_000 }).catch(() => false))) { const toggle = page.locator('button, label').filter({ hasText: /extern|alle/i }).first() if (await toggle.isVisible()) await toggle.click() } await expect(placeholder.first()).toBeVisible({ timeout: 8_000 }) }) test('UI-POLISH: namenloser Knoten im Stammbaum zeigt "(ohne Namen)"', async ({ page }) => { skipUnlessLive() // Stammbaum eines Tiers mit namenlosem Elternteil // Tier "46c646a9" ist namenlos + resident; sein Elternteil im Stammbaum-Kontext // Alternativ: Vance's Stammbaum enthält tiefe Äste, manche namenlos await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e/stammbaum') await expect(page.locator('svg').first()).toBeVisible({ timeout: 12_000 }) // Prüfe ob "(ohne Namen)" irgendwo im Baum erscheint const placeholder = page.getByText('(ohne Namen)') // Kein harter Fail wenn kein namenloser Knoten sichtbar — Tree kann mit Lazy-Gen-5+ variieren // Aber wenn vorhanden, muss der Text sauber sein (nicht leer) const visible = await placeholder.isVisible({ timeout: 5_000 }).catch(() => false) if (visible) { await expect(placeholder.first()).toBeVisible() } // Kein JS-Error auf der Seite (implizit durch erfolgreiche Navigation) }) // ── 5. GEHEGE: Live-Verifikation der Redirect/Nav-Änderungen ───────────────── test('GEHEGE live: /becken leitet auf /gehege weiter', async ({ page }) => { skipUnlessLive() await page.goto('/becken') await expect(page).toHaveURL(/\/gehege$/, { timeout: 5_000 }) await expect(page.getByRole('heading', { name: de.pages.becken.title, exact: true })).toBeVisible() }) test('GEHEGE live: "/" leitet auf /rennmaeuse weiter (kein Start-Screen)', async ({ page }) => { skipUnlessLive() await page.goto('/') await expect(page).toHaveURL(/\/rennmaeuse$/, { timeout: 5_000 }) await expect( page.getByRole('heading', { name: de.pages.gerbils.title, exact: true }), ).toBeVisible() })