Compare commits
9 Commits
feature/ge
...
01781f23d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 01781f23d6 | |||
| e1e088f041 | |||
| 0b9b03eb2d | |||
| d4c9be23c8 | |||
| f6bfc0ebab | |||
| 3160b66697 | |||
| 39767cef0f | |||
| c26ac5c6dd | |||
| 2c3f342905 |
@@ -55,7 +55,7 @@ export function skipUnlessLive() {
|
||||
* Kontakte und Statistik hinter dem „Mehr“-Blatt.
|
||||
*/
|
||||
export async function gotoSection(page: Page, label: string) {
|
||||
const nav = page.getByRole('navigation')
|
||||
const nav = page.getByRole('navigation', { name: de.nav.mainNavigation })
|
||||
// App noch nicht geladen (about:blank) -> erst zur Startseite
|
||||
if (!(await nav.isVisible())) {
|
||||
await page.goto('/')
|
||||
|
||||
90
gerbil-manager-web/e2e/gehege-regression.spec.ts
Normal file
90
gerbil-manager-web/e2e/gehege-regression.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* QA-REGRESSION-1: GEHEGE-RENAME Lock-in.
|
||||
*
|
||||
* Sichert das Verhalten von drei GEHEGE-RENAME-Änderungen ab, die leicht still
|
||||
* brechen können (Redirects, Nav-Einträge, Landing-URL):
|
||||
* (a) /becken/* → /gehege/* Redirect (bookmarks / externe Links)
|
||||
* (b) Nav zeigt 'Gehege' + 🏜️, kein 'Becken', kein 'Start'-Eintrag
|
||||
* (c) '/' (Index) landet auf /rennmaeuse (nicht mehr auf einer Startseite)
|
||||
*
|
||||
* Bereits abgedeckt (NICHT dupliziert):
|
||||
* - gotoSection(de.nav.enclosures) navigiert zu Gehege-Heading: navigation.spec.ts:19-20
|
||||
* - CRUD + 409 auf /gehege: becken-kontakte.spec.ts
|
||||
* - Wurf-Link + Herkunft: tiere.spec.ts:101-128
|
||||
*/
|
||||
import { de, expect, test } from './fixtures'
|
||||
|
||||
// ── (a) /becken/* → /gehege/* Redirect ──────────────────────────────────────
|
||||
|
||||
test('GEHEGE-RENAME: /becken leitet auf /gehege weiter (bookmark-safe)', async ({ page }) => {
|
||||
await page.goto('/becken')
|
||||
// React Router BeckenRedirect navigiert → /gehege; Gehege-Heading muss sichtbar sein
|
||||
await expect(page.getByRole('heading', { name: de.pages.becken.title, exact: true })).toBeVisible()
|
||||
await expect(page).toHaveURL(/\/gehege$/)
|
||||
})
|
||||
|
||||
test('GEHEGE-RENAME: /becken/neu leitet auf /gehege/neu weiter (deep redirect)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/becken/neu')
|
||||
// Neues-Gehege-Formular muss sichtbar sein, URL muss /gehege/neu sein
|
||||
await expect(page.getByRole('heading', { name: de.pages.becken.form?.createTitle ?? de.pages.becken.newButton })).toBeVisible({ timeout: 6_000 })
|
||||
await expect(page).toHaveURL(/\/gehege\/neu$/)
|
||||
})
|
||||
|
||||
// ── (b) Nav: 'Gehege' ✓, 'Becken' ✗, 'Start' ✗ ──────────────────────────────
|
||||
|
||||
test('GEHEGE-RENAME: Nav hat "Gehege"-Eintrag mit Terrarium-Icon, kein "Becken"', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/rennmaeuse')
|
||||
const nav = page.getByRole('navigation', { name: de.nav.mainNavigation })
|
||||
|
||||
// Gehege-Link soll vorhanden sein (via „Mehr"-Blatt auf Phone oder direkt auf Desktop)
|
||||
// Wir öffnen „Mehr" um sekundäre Links sichtbar zu machen
|
||||
const moreBtn = nav.getByRole('button', { name: de.nav.more })
|
||||
if (await moreBtn.isVisible()) {
|
||||
await moreBtn.click()
|
||||
}
|
||||
await expect(nav.getByRole('link', { name: de.nav.enclosures })).toBeVisible()
|
||||
// "Becken" als separater Nav-Eintrag darf nicht mehr existieren
|
||||
await expect(nav.getByRole('link', { name: 'Becken', exact: true })).toBeHidden()
|
||||
})
|
||||
|
||||
test('GEHEGE-RENAME: Nav hat keinen "Start"-Eintrag mehr (de.nav.home entfernt)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/rennmaeuse')
|
||||
// Warten bis die Tierliste geladen ist, dann Nav prüfen
|
||||
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
|
||||
const nav = page.getByRole('navigation', { name: de.nav.mainNavigation })
|
||||
// 'Start' (de.nav.home wurde entfernt) darf kein Nav-Link mehr sein
|
||||
await expect(nav.getByRole('link', { name: 'Start', exact: true })).toBeHidden()
|
||||
})
|
||||
|
||||
// ── (c) '/' landet auf /rennmaeuse ──────────────────────────────────────────
|
||||
|
||||
test('GEHEGE-RENAME: "/" leitet auf /rennmaeuse weiter — keine Startseite mehr', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
// URL muss /rennmaeuse sein (Navigate replace)
|
||||
await expect(page).toHaveURL(/\/rennmaeuse$/)
|
||||
// Tierliste-Heading sichtbar — nicht die alte Startseite
|
||||
await expect(
|
||||
page.getByRole('heading', { name: de.pages.gerbils.title, exact: true }),
|
||||
).toBeVisible()
|
||||
// Alten Willkommen-Text darf es nicht mehr geben (de.pages.home wurde entfernt)
|
||||
await expect(page.getByText('Willkommen')).toBeHidden()
|
||||
})
|
||||
|
||||
test('GEHEGE-RENAME: 404-Seite "Zur Startseite" landet weiterhin auf Tierliste', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/diese-seite-gibt-es-nicht')
|
||||
await expect(page.getByText(de.pages.notFound.message)).toBeVisible()
|
||||
await page.getByRole('link', { name: de.pages.notFound.backHome }).click()
|
||||
// Nach Klick auf "Zur Startseite" → '/' → redirect → /rennmaeuse
|
||||
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
|
||||
await expect(page).toHaveURL(/\/rennmaeuse$/)
|
||||
})
|
||||
302
gerbil-manager-web/e2e/live-data.spec.ts
Normal file
302
gerbil-manager-web/e2e/live-data.spec.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* QA-LIVE-DATA: Tiefe Begehung mit echten Import-Daten (325 Tiere, 865 Würfe, 138 Fotos).
|
||||
* Läuft ausschließlich im LIVE-Modus (E2E_BASE_URL gesetzt).
|
||||
*
|
||||
* Bekannte Datengrundlage (Stand Re-Import #2.5):
|
||||
* - Vance von den Kleinen Chaoten id=afdaff89-0274-4778-8f2f-3957a81bf58e (Resident, Stammbaum)
|
||||
* - Girasol of Topolino id=bf20f105-2ea9-475f-89b7-d50e59fb7d3b (9 Fotos)
|
||||
* - Litter 2026-02-10 id=a1db02cf-48ff-4f3a-8eab-33f2360c27f0 (beide Eltern gesetzt)
|
||||
* - 204 Residents, 121 Externe, 15 namenlose Einträge
|
||||
*/
|
||||
import { de, expect, gotoSection, openFilterPanel, skipUnlessLive, test } from './fixtures'
|
||||
|
||||
const t = de.pages.gerbils
|
||||
const tl = de.pages.litters
|
||||
const td = t.detail
|
||||
|
||||
// ── 1. RENNMÄUSE-LISTE ───────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Tiere-Liste lädt 325 Einträge (Bestand-Default-Filter inklusive externe)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
const rows = page.locator('.gerbil-row')
|
||||
await expect(rows.first()).toBeVisible()
|
||||
const count = await rows.count()
|
||||
// Alle 325 oder zumindest Paginierung der ersten Seite (>=20)
|
||||
expect(count).toBeGreaterThanOrEqual(20)
|
||||
// Count-Label sichtbar ("204 Tiere" für Bestand-Default-Filter oder "325 Tiere")
|
||||
await expect(page.locator('p.muted').filter({ hasText: t.countLabel })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Live-Data: Bestand-Filter sichtbar + umschaltbar (Resident vs. Alle)', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
await openFilterPanel(page)
|
||||
// Filter-Panel oder mindestens der "Nur Bestand"-Toggle ist sichtbar
|
||||
const filterSection = page.locator('.filter-panel, [aria-label*="Filter"], .filter-panel__content')
|
||||
await expect(filterSection.first()).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
test('Live-Data: Suche nach "Vance" findet mindestens einen Treffer', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
await page.getByPlaceholder(t.searchPlaceholder).first().fill('Vance')
|
||||
const rows = page.locator('.gerbil-row')
|
||||
await expect(rows.first()).toBeVisible({ timeout: 10_000 })
|
||||
expect(await rows.count()).toBeGreaterThanOrEqual(1)
|
||||
// Name "Vance" soll in einem Ergebnis auftauchen
|
||||
await expect(page.locator('.gerbil-row').filter({ hasText: 'Vance' }).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Live-Data: Suche nach "von den" (Namensmuster) liefert Treffer', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
await page.getByPlaceholder(t.searchPlaceholder).first().fill('von den')
|
||||
const rows = page.locator('.gerbil-row')
|
||||
await expect(rows.first()).toBeVisible({ timeout: 10_000 })
|
||||
expect(await rows.count()).toBeGreaterThanOrEqual(5)
|
||||
})
|
||||
|
||||
test('Live-Data: Genotyp-Tabelle scrollt horizontal (kein Page-Overflow bei 390px)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.locator('.gerbil-row').first()).toBeVisible()
|
||||
// Kein horizontaler Scroll auf body/html
|
||||
const bodyOverflow = await page.evaluate(() => {
|
||||
const body = document.body
|
||||
const html = document.documentElement
|
||||
return {
|
||||
bodyScrollWidth: body.scrollWidth,
|
||||
htmlScrollWidth: html.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
}
|
||||
})
|
||||
expect(bodyOverflow.bodyScrollWidth).toBeLessThanOrEqual(bodyOverflow.viewportWidth + 2)
|
||||
expect(bodyOverflow.htmlScrollWidth).toBeLessThanOrEqual(bodyOverflow.viewportWidth + 2)
|
||||
})
|
||||
|
||||
// ── 2. TIER-DETAIL ────────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Girasol of Topolino Detail zeigt Fotos-Tab mit mindestens 9 Fotos', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
await page.goto('/rennmaeuse/bf20f105-2ea9-475f-89b7-d50e59fb7d3b')
|
||||
await expect(page.getByRole('heading', { name: 'Girasol of Topolino' })).toBeVisible()
|
||||
// Fotos-Tab anklicken
|
||||
await page.getByRole('tab', { name: td.tabs.photos }).click()
|
||||
// Mindestens ein Foto-Img sichtbar (9 Fotos importiert)
|
||||
const photos = page.locator('img.photo-thumb, img[src*="/photos/files/"], .photo-item img, .photo-list img')
|
||||
await expect(photos.first()).toBeVisible({ timeout: 8_000 })
|
||||
expect(await photos.count()).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('Live-Data: Vance von den Kleinen Chaoten Detail rendert korrekt', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e')
|
||||
await expect(page.getByRole('heading', { name: 'Vance von den Kleinen Chaoten' })).toBeVisible()
|
||||
// Gesundheits-Tab (leer OK, soll aber nicht crashen)
|
||||
await page.getByRole('tab', { name: td.tabs.health }).click()
|
||||
await expect(page).not.toHaveURL('/error')
|
||||
// Gewicht-Tab
|
||||
await page.getByRole('tab', { name: td.tabs.weight }).click()
|
||||
await expect(page).not.toHaveURL('/error')
|
||||
})
|
||||
|
||||
test('Live-Data: Stammbaum-Link von Tier-Detail navigiert zu StammbaumPage', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e')
|
||||
await expect(page.getByRole('heading', { name: 'Vance von den Kleinen Chaoten' })).toBeVisible()
|
||||
// Stammbaum-Link suchen
|
||||
const stammbaumLink = page.getByRole('link', { name: /Stammbaum/i }).first()
|
||||
if (await stammbaumLink.isVisible()) {
|
||||
await stammbaumLink.click()
|
||||
await expect(page).toHaveURL(/stammbaum/)
|
||||
} else {
|
||||
// Direkt navigieren
|
||||
await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e/stammbaum')
|
||||
}
|
||||
// Stammbaum-SVG oder rd3t-Knoten sichtbar
|
||||
await expect(page.locator('[data-testid="rd3t-svg"], svg.rd3t-svg, .stammbaum-page, svg').first()).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
|
||||
// ── 3. STAMMBAUM-SEITE ───────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Stammbaum rendert ohne Page-Overflow (390px)', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e/stammbaum')
|
||||
// Irgendein SVG-Element muss sichtbar sein
|
||||
await expect(page.locator('svg').first()).toBeVisible({ timeout: 12_000 })
|
||||
const overflow = await page.evaluate(() => ({
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
}))
|
||||
expect(overflow.bodyScrollWidth).toBeLessThanOrEqual(overflow.viewportWidth + 2)
|
||||
})
|
||||
|
||||
test('Live-Data: Stammbaum hat Druck-Button', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await page.goto('/rennmaeuse/afdaff89-0274-4778-8f2f-3957a81bf58e/stammbaum')
|
||||
await expect(page.locator('svg').first()).toBeVisible({ timeout: 12_000 })
|
||||
const printBtn = page.locator('button, a').filter({ hasText: /Drucken|Print|Ahnentafel/i })
|
||||
await expect(printBtn.first()).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 4. WÜRFE-LISTE ───────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Würfe-Liste zeigt mindestens 20 Einträge (865 Würfe importiert)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.litters)
|
||||
await expect(page.getByRole('heading', { name: tl.title, exact: true })).toBeVisible()
|
||||
const rows = page.locator('.litter-row, .gerbil-card, li a').filter({ hasText: /Wurf/i })
|
||||
await expect(rows.first()).toBeVisible()
|
||||
expect(await rows.count()).toBeGreaterThanOrEqual(20)
|
||||
})
|
||||
|
||||
test('Live-Data: Wurf-Detail mit beiden Eltern rendert "Elterntiere"', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
// Direkt zu einem Wurf mit beiden Eltern navigieren (2026-02-10)
|
||||
await page.goto('/wuerfe/a1db02cf-48ff-4f3a-8eab-33f2360c27f0')
|
||||
// Elterntiere-Section immer sichtbar
|
||||
await expect(page.getByText(tl.detail.parents, { exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Vater-Link zu einem Tier
|
||||
await expect(page.locator('a[href*="/rennmaeuse/"]').first()).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
test('Live-Data: Wurf-Detail ohne Eltern rendert "Elterntiere" als "unbekannt"', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
// Wurf A (2010-02-18) hat weder Vater noch Mutter
|
||||
await page.goto('/wuerfe/34852596-2bd7-48ad-9175-4223d3d99229')
|
||||
await expect(page.getByText(tl.detail.parents, { exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// "unbekannt" Platzhalter soll sichtbar sein (kein Link)
|
||||
await expect(page.getByText(tl.detail.unknownParent ?? 'unbekannt')).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 5. BECKEN / KONTAKTE (leere Zustände) ────────────────────────────────────
|
||||
|
||||
test('Live-Data: Becken-Seite zeigt Leer-Zustand (keine Becken importiert)', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.enclosures)
|
||||
await expect(page.getByRole('heading', { name: de.pages.becken.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Leer-Meldung oder "Neues Becken"-Button
|
||||
const emptyOrNew = page.locator('*').filter({ hasText: de.pages.becken.empty }).or(
|
||||
page.getByRole('link', { name: de.pages.becken.newButton })
|
||||
)
|
||||
await expect(emptyOrNew.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Live-Data: Kontakte-Seite zeigt Leer-Zustand (keine Kontakte importiert)', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.contacts)
|
||||
await expect(page.getByRole('heading', { name: de.pages.kontakte.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
await expect(page.getByText(de.pages.kontakte.empty)).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 6. GENETIK / PROBEVERPAARUNG ─────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Genetik-Seite lädt mit echten Genotypen', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.genetics)
|
||||
await expect(page.getByRole('heading', { name: de.pages.genetik.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Probeverpaarung-UI sichtbar
|
||||
await expect(page.getByText(de.pages.genetik.subtitle)).toBeVisible()
|
||||
// Tier-Auswahl Vater/Mutter sichtbar
|
||||
await expect(page.locator('select, input[type="search"], [placeholder]').first()).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 7. STATISTIK ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Statistik-Seite rendert Charts ohne Crash', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.statistics)
|
||||
await expect(page.getByRole('heading', { name: de.pages.statistik.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Mindestens ein Chart-Canvas oder SVG (865 Würfe = Charts vorhanden)
|
||||
const chart = page.locator('canvas, svg.bar-chart, svg.line-chart, .chart-wrapper, .bar-chart, .line-chart')
|
||||
await expect(chart.first()).toBeVisible({ timeout: 12_000 })
|
||||
})
|
||||
|
||||
// ── 8. EINSTELLUNGEN ─────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Einstellungen-Seite lädt Züchterprofil', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.settings)
|
||||
await expect(page.getByRole('heading', { name: de.pages.einstellungen.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
await expect(page.getByText(de.pages.einstellungen.zuchtprofil.title)).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 9. WEBSEITE (CMS) ────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Webseite-Seite lädt ohne Crash', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.website)
|
||||
await expect(page.getByRole('heading', { name: de.pages.webseite.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Seitenliste mit Seeds vorhanden (8 Seiten aus dem Seed)
|
||||
await expect(page.locator('.gerbil-card, li').first()).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
// ── 10. ANFRAGEN ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Anfragen-Seite zeigt Leer-Zustand (0 Anfragen)', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.requests)
|
||||
await expect(page.getByRole('heading', { name: de.pages.anfragen.title, exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Leer-Text soll sichtbar sein (keine Anfragen live)
|
||||
await expect(page.getByText(de.pages.anfragen.empty)).toBeVisible()
|
||||
})
|
||||
|
||||
// ── 11. KONSOLEN-FEHLER-MONITOR ───────────────────────────────────────────────
|
||||
|
||||
test('Live-Data: Keine unhandled-rejection-Fehler beim Durchlauf aller Hauptseiten', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => errors.push(err.message))
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') errors.push(`[console.error] ${msg.text()}`)
|
||||
})
|
||||
|
||||
// Navigiere direkt per URL statt gotoSection — gotoSection benutzt getByRole('navigation')
|
||||
// welches bei Seiten mit Pager-Navigation (>1 Page) in strict-mode fehlschlägt (Infra-Bug).
|
||||
for (const path of ['/', '/rennmaeuse', '/statistik', '/genetik', '/einstellungen']) {
|
||||
await page.goto(path)
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
// Keine unkritischen Errors (ignoriere CORS-Warnungen, die im Dev-Modus normal sind)
|
||||
const criticalErrors = errors.filter(
|
||||
(e) =>
|
||||
!e.includes('CORS') &&
|
||||
!e.includes('favicon') &&
|
||||
!e.includes('DevTools') &&
|
||||
!e.includes('Warning:'),
|
||||
)
|
||||
expect(criticalErrors).toHaveLength(0)
|
||||
})
|
||||
|
||||
// ── 12. DESKTOP-OVERFLOW-CHECK ───────────────────────────────────────────────
|
||||
// (dieses Projekt-File läuft in beiden Viewports dank playwright.config.ts)
|
||||
|
||||
test('Live-Data: Rennmäuse-Liste kein horizontal Overflow (Desktop 1280px)', async ({ page }) => {
|
||||
skipUnlessLive()
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.locator('.gerbil-row').first()).toBeVisible()
|
||||
const overflow = await page.evaluate(() => ({
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
}))
|
||||
expect(overflow.scrollWidth).toBeLessThanOrEqual(overflow.clientWidth + 2)
|
||||
})
|
||||
@@ -40,18 +40,13 @@ test('Live: Wurf-Detail zeigt Elterntier-Links (mindestens Vater oder Mutter)',
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessLive()
|
||||
// Nach Re-Import #2 hat mindestens Gerbil C (Victoria) beide Eltern.
|
||||
// Der Wurf-Detail ist die einfachste Ansicht, die Eltern-Links rendert.
|
||||
await gotoSection(page, de.nav.litters)
|
||||
await expect(page.getByRole('heading', { name: tl.title, exact: true })).toBeVisible()
|
||||
// Direkt zu einem Wurf mit beiden Eltern navigieren (Re-Import #2.5, stabil).
|
||||
// Wurf (aus Diagramm) 2026-02-10 — fatherId + motherId beide gesetzt.
|
||||
// Kein nth()-Selektor mehr: bei 865 Würfen + Pager-Nav nicht deterministisch.
|
||||
await page.goto('/wuerfe/a1db02cf-48ff-4f3a-8eab-33f2360c27f0')
|
||||
|
||||
// Mindestens 1 Wurf in der Liste; ersten öffnen.
|
||||
const firstLink = page.getByRole('link').filter({ hasText: /.+/ }).nth(1)
|
||||
await expect(firstLink).toBeVisible()
|
||||
await firstLink.click()
|
||||
|
||||
// Detailseite: Elterntier-Abschnitt ist sichtbar
|
||||
await expect(page.getByText(tl.detail.parents)).toBeVisible()
|
||||
// Detailseite: Elterntier-Abschnitt immer sichtbar (kein bedingtes Rendering)
|
||||
await expect(page.getByText(tl.detail.parents, { exact: true })).toBeVisible({ timeout: 8_000 })
|
||||
// Mindestens ein Link führt zu einem Tier — bei fehlendem Import fehlt dieser Link
|
||||
await expect(page.locator('a[href*="/rennmaeuse/"]').first()).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
@@ -98,6 +98,35 @@ test('Detailseite zeigt Stammdaten + Tab-Inhalte (Gesundheit/Gewicht/Fotos)', as
|
||||
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: 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('Tier bearbeiten — Notizen ändern', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
||||
|
||||
@@ -173,8 +173,22 @@ export default function GerbilDetailPage() {
|
||||
}
|
||||
/>
|
||||
<Row label={t.fields.enclosure} value={lookup(enclosureName, g.enclosureId)} />
|
||||
<Row label={t.fields.litter} value={lookup(litterName, g.litterId)} />
|
||||
<Row label={t.fields.origin} value={lookup(contactName, g.originContactId)} />
|
||||
<Row
|
||||
label={t.fields.litter}
|
||||
value={
|
||||
g.litterId && litterName.has(g.litterId)
|
||||
? <Link to={`/wuerfe/${g.litterId}`}>{litterName.get(g.litterId)}</Link>
|
||||
: lookup(litterName, g.litterId)
|
||||
}
|
||||
/>
|
||||
<Row
|
||||
label={t.fields.origin}
|
||||
value={
|
||||
g.originContactId && contactName.has(g.originContactId)
|
||||
? <Link to={`/kontakte/${g.originContactId}`}>{contactName.get(g.originContactId)}</Link>
|
||||
: (g.originBreeder || null)
|
||||
}
|
||||
/>
|
||||
<Row label={t.fields.receiver} value={lookup(contactName, g.receiverContactId)} />
|
||||
<Row label={t.fields.notes} value={g.notes} />
|
||||
</dl>
|
||||
|
||||
Reference in New Issue
Block a user