From d1baba686ff4c7daf4bdbc87a0836f2e2dfa8050 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 19:51:45 +0200 Subject: [PATCH] QA-SMOKE-2: Live-Smoke GEN-3h/FORM-FIELDS/CR-2/UI-POLISH/GEHEGE (18/18 gruen) --- gerbil-manager-web/e2e/smoke-2.spec.ts | 169 +++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 gerbil-manager-web/e2e/smoke-2.spec.ts diff --git a/gerbil-manager-web/e2e/smoke-2.spec.ts b/gerbil-manager-web/e2e/smoke-2.spec.ts new file mode 100644 index 0000000..0fb97f6 --- /dev/null +++ b/gerbil-manager-web/e2e/smoke-2.spec.ts @@ -0,0 +1,169 @@ +/** + * 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() +})