const { test, expect } = require('@playwright/test'); const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com'; const TEST_PASSWORD = process.env.TEST_PASSWORD || '12345678'; /** Primärer Submit auf der Login-Seite (nicht den Tab "Login" vs. "Registrieren"). */ async function submitLoginForm(page) { await page.getByRole('button', { name: 'Anmelden' }).click(); } async function login(page) { await page.goto('/'); await page.waitForLoadState('networkidle'); // Warte bis Login-Seite geladen ist await page.waitForSelector('input[type="email"]', { timeout: 10000 }); await page.fill('input[type="email"]', TEST_EMAIL); await page.fill('input[type="password"]', TEST_PASSWORD); await submitLoginForm(page); // Wait until auth is complete: URL leaves /login and Dashboard is rendered await page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 15000 }); await page.waitForLoadState('networkidle'); } test('1. Login funktioniert', async ({ page }) => { await page.goto('/'); await page.waitForSelector('input[type="email"]', { timeout: 10000 }); await page.fill('input[type="email"]', TEST_EMAIL); await page.fill('input[type="password"]', TEST_PASSWORD); await submitLoginForm(page); await page.waitForLoadState('networkidle'); // Nach Login soll der Tab "Login" (Moduswahl) verschwinden — nicht der Submit "Anmelden" const loginButton = page.locator('button:has-text("Login")'); await expect(loginButton).toHaveCount(0, { timeout: 10000 }); await page.screenshot({ path: 'screenshots/01-nach-login.png' }); console.log('✓ Login erfolgreich'); }); test('2. Dashboard lädt ohne Fehler', async ({ page }) => { await login(page); // Warte bis Spinner verschwunden await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); // Dashboard: h1 „Dashboard“ + Begrüßungstext (nicht mehr „Willkommen bei Shinkan“ als Überschrift) const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: 'Dashboard' })).toBeVisible({ timeout: 5000, }); await expect(main.getByText(/Shinkan unterstützt dich/i)).toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'screenshots/02-dashboard.png' }); console.log('✓ Dashboard OK'); }); test('3. Navigation zu Übungen', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); // Bei Viewport ≥1024px ist .bottom-nav versteckt — Mobile garantieren wie in playwright.config.js await page.setViewportSize({ width: 390, height: 844 }); // Bottom-Nav: Navigation und URL gemeinsam abwarten (vermeidet race mit networkidle) const exercisesLink = page.locator('.bottom-nav').getByRole('link', { name: /Übungen/i }); await Promise.all([ page.waitForURL( (u) => { const path = u.pathname.replace(/\/$/, '') || '/' return path === '/exercises' }, { timeout: 15000 }, ), exercisesLink.click(), ]); await page.waitForLoadState('networkidle'); // Wie Test 4 (Vereine): eine eindeutige h1 — nicht h1,h2-Kombi (Strict Mode + mehrere Treffer) const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: /Übungen/i })).toBeVisible({ timeout: 10000, }); await page.screenshot({ path: 'screenshots/03-uebungen.png' }); console.log('✓ Übungen-Seite erreichbar'); }); test('4. Navigation zu Vereine', async ({ page }) => { await login(page); await page.setViewportSize({ width: 390, height: 844 }); await page.locator('.bottom-nav a[href="/clubs"]').click(); await page.waitForLoadState('networkidle'); // ClubsPage:

Vereinsverwaltung

+ Tab

Vereine

→ ein kombinierter // Selektor löst 2 Treffer aus (Playwright strict mode). URL + primäre Überschrift reichen. await expect(page).toHaveURL(/\/clubs\/?$/, { timeout: 5000 }); await expect(page.getByRole('heading', { level: 1, name: /Vereinsverwaltung/i })).toBeVisible({ timeout: 5000, }); await page.screenshot({ path: 'screenshots/04-vereine.png' }); console.log('✓ Vereine-Seite erreichbar'); }); test('5. Desktop-Sidebar sichtbar (Desktop)', async ({ page }) => { // Desktop-Viewport await page.setViewportSize({ width: 1280, height: 800 }); await login(page); // Prüfe ob Desktop-Sidebar existiert const sidebar = page.locator('.desktop-sidebar'); await expect(sidebar).toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'screenshots/05-desktop-sidebar.png' }); console.log('✓ Desktop-Sidebar sichtbar'); }); test('6. Bottom-Nav sichtbar (Mobile)', async ({ page }) => { // Mobile-Viewport await page.setViewportSize({ width: 390, height: 844 }); await login(page); // Prüfe ob Bottom-Nav existiert const bottomNav = page.locator('.bottom-nav'); await expect(bottomNav).toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'screenshots/06-mobile-bottom-nav.png' }); console.log('✓ Bottom-Nav sichtbar'); }); test('7. Session-Persistenz nach Reload', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await expect( page.locator('.app-main').getByRole('heading', { level: 1, name: 'Dashboard' }), ).toBeVisible({ timeout: 10000 }); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle'); // Auth lädt erst nach Spinner – nicht auf /login stranden (stabiler als Button „Login“-Tab auf Login-Screen) await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 20000 }); await expect(page).not.toHaveURL(/\/login(?:\/|$|\?|#)/, { timeout: 20000 }); await expect( page.locator('.app-main').getByRole('heading', { level: 1, name: 'Dashboard' }), ).toBeVisible({ timeout: 20000, }); await page.screenshot({ path: 'screenshots/07-nach-reload.png' }); console.log('✓ Session bleibt nach Reload erhalten'); }); /** * Phase 2 (Dashboard): ein GET /api/dashboard/kpis (KPIs + Trainings-Home); keine direkten GET /api/training-units vom Dashboard. * Production-ähnlicher Build empfohlen (kein React StrictMode-Doppel-Mount im lokalen Vite-Dev). */ test('8. Dashboard API-Budget nach Reload (profiles/me, dashboard/kpis)', async ({ page }) => { await login(page); let profilesMe = 0; let trainingUnits = 0; let dashboardKpis = 0; const onRequest = (request) => { if (request.method() !== 'GET') return; let pathname = ''; try { pathname = new URL(request.url()).pathname; } catch { return; } if (pathname === '/api/profiles/me') profilesMe += 1; if (pathname === '/api/training-units') trainingUnits += 1; if (pathname === '/api/dashboard/kpis') dashboardKpis += 1; }; page.on('request', onRequest); const kpisStatuses = []; const onResponse = (response) => { try { const u = response.url(); if (u.includes('/api/dashboard/kpis')) kpisStatuses.push(response.status()); } catch { /* ignore */ } }; page.on('response', onResponse); try { await page.reload({ waitUntil: 'networkidle' }); const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: 'Dashboard' })).toBeVisible({ timeout: 15000, }); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await expect(page.getByRole('heading', { name: 'Nächste Termine' })).toBeVisible({ timeout: 20000, }); expect(profilesMe).toBe(1); expect(trainingUnits).toBe(0); expect(dashboardKpis).toBe(1); expect(kpisStatuses.some((s) => s === 200)).toBe(true); } finally { page.off('request', onRequest); page.off('response', onResponse); } console.log('✓ Dashboard API-Budget: 1× profiles/me, 0× training-units, 1× dashboard/kpis (HTTP 200)'); }); test('9. Übungsliste: nach Laden entweder Treffer-Gitter oder Leerhinweis', async ({ page }) => { await login(page); await page.goto('/exercises', { waitUntil: 'networkidle' }); const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: /Übungen/i })).toBeVisible({ timeout: 15000, }); await expect(main.locator('.spinner')).toHaveCount(0, { timeout: 20000 }); const grid = main.getByTestId('exercises-list-grid'); const empty = main.locator('.exercises-empty-text'); await expect(grid.or(empty).first()).toBeVisible({ timeout: 15000 }); console.log('✓ Übungsliste: Endzustand sichtbar (Gitter oder leer)'); }); test('10. Übungsliste: Filter-Dialog öffnet und schließt', async ({ page }) => { await login(page); await page.goto('/exercises', { waitUntil: 'networkidle' }); const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: /Übungen/i })).toBeVisible({ timeout: 15000, }); await expect(main.locator('.spinner')).toHaveCount(0, { timeout: 20000 }); await main.getByRole('button', { name: /^Filter$/i }).click(); const dlg = page.getByTestId('exercise-list-filter-modal'); await expect(dlg).toBeVisible({ timeout: 10000 }); await expect(dlg.getByRole('heading', { name: 'Übungen filtern' })).toBeVisible(); await dlg.getByRole('button', { name: 'Schließen' }).click(); await expect(dlg).toHaveCount(0); console.log('✓ Übungsliste: Filter-Dialog Smoke'); }); test('11. Übungsliste: Massenauswahl zeigt Bulk-Toolbar', async ({ page }, testInfo) => { await login(page); await page.goto('/exercises', { waitUntil: 'networkidle' }); const main = page.locator('.app-main'); await expect(main.getByRole('heading', { level: 1, name: /Übungen/i })).toBeVisible({ timeout: 15000, }); await expect(main.locator('.spinner')).toHaveCount(0, { timeout: 20000 }); const grid = main.getByTestId('exercises-list-grid'); const checks = grid.locator('input[type="checkbox"]'); const n = await checks.count(); if (n < 1) { testInfo.skip(true, 'Keine Übung in der Liste (Bulk-Smoke braucht mind. einen Treffer)'); return; } await checks.first().click(); const bulk = main.getByTestId('exercise-list-bulk-toolbar'); await expect(bulk).toBeVisible({ timeout: 5000 }); await expect(bulk.getByRole('button', { name: /Massenänderung/i })).toBeVisible(); console.log('✓ Übungsliste: Bulk-Toolbar nach Auswahl'); }); test('12. Trainingsplanung: Seite lädt mit Überschrift', async ({ page }) => { await login(page); await page.goto('/planning', { waitUntil: 'networkidle' }); const main = page.locator('.app-main'); await expect(main.locator('.spinner')).toHaveCount(0, { timeout: 25000 }); await expect(main.getByRole('heading', { level: 1, name: 'Trainingsplanung' })).toBeVisible({ timeout: 20000, }); console.log('✓ Trainingsplanung: Grundansicht'); }); test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await login(page); await page.waitForLoadState('networkidle'); // Coach-Sitzungsdaten simulieren (wie sie TrainingCoachPage schreibt) await page.evaluate(() => { sessionStorage.setItem('sj_coach_step_42', '3'); sessionStorage.setItem('sj_coach_deltas_42', '[{"id":1}]'); sessionStorage.setItem('sj_coach_debrief_42', '0'); sessionStorage.setItem('fremd_anderer_key', 'muss_erhalten_bleiben'); }); const vorLogout = await page.evaluate(() => ({ step: sessionStorage.getItem('sj_coach_step_42'), fremd: sessionStorage.getItem('fremd_anderer_key'), })); expect(vorLogout.step).toBe('3'); expect(vorLogout.fremd).toBe('muss_erhalten_bleiben'); // App.jsx DesktopSidebar-Logout zeigt confirm() — in Playwright headless akzeptieren page.once('dialog', dialog => dialog.accept()); await page.getByRole('button', { name: 'Abmelden' }).click(); await page.waitForLoadState('networkidle'); const nachLogout = await page.evaluate(() => ({ step: sessionStorage.getItem('sj_coach_step_42'), deltas: sessionStorage.getItem('sj_coach_deltas_42'), debrief: sessionStorage.getItem('sj_coach_debrief_42'), fremd: sessionStorage.getItem('fremd_anderer_key'), token: localStorage.getItem('authToken'), })); expect(nachLogout.step).toBeNull(); expect(nachLogout.deltas).toBeNull(); expect(nachLogout.debrief).toBeNull(); expect(nachLogout.fremd).toBe('muss_erhalten_bleiben'); expect(nachLogout.token).toBeNull(); await page.screenshot({ path: 'screenshots/p12-session-storage-nach-logout.png' }); console.log('✓ P-12: sj_coach_* entfernt, Fremd-Key erhalten, authToken entfernt'); }); // P-01: Rechtstextseiten – öffentliche Routen ohne Auth const LEGAL_ROUTES = [ { path: '/impressum', label: 'Impressum' }, { path: '/datenschutz', label: 'Datenschutz' }, { path: '/nutzungsbedingungen', label: 'Nutzungsbedingungen' }, { path: '/medienrichtlinie', label: 'Medienrichtlinie' }, ]; for (const route of LEGAL_ROUTES) { test(`P-01: ${route.label} ohne Auth erreichbar und enthält Platzhalterhinweis`, async ({ page }) => { // Direkt aufrufen ohne Login await page.goto(route.path); await page.waitForLoadState('networkidle'); // Seite ist erreichbar (kein Redirect zur Login-Seite) expect(page.url()).toContain(route.path); // Seitentitel korrekt await expect(page.getByRole('heading', { level: 1 })).toContainText(route.label); // Platzhalterhinweis sichtbar – nur wenn noch kein echtes Dokument veröffentlicht wurde const hasPlaceholder = await page.getByText('MUSTER / PLATZHALTER').first().isVisible().catch(() => false); if (hasPlaceholder) { console.log(`✓ P-01: ${route.label} – Platzhalter sichtbar`); } else { console.log(`✓ P-01: ${route.label} – echtes Dokument veröffentlicht (kein Platzhalter)`); } // Reload funktioniert await page.reload(); await page.waitForLoadState('networkidle'); expect(page.url()).toContain(route.path); console.log(`✓ P-01: ${route.label} – ohne Auth erreichbar, Reload OK`); }); } test('P-01: Login-Seite enthält Links zu allen vier Rechtstextseiten', async ({ page }) => { await page.goto('/login'); await page.waitForLoadState('networkidle'); for (const route of LEGAL_ROUTES) { const link = page.locator(`a[href="${route.path}"]`); await expect(link).toBeVisible(); } console.log('✓ P-01: Login-Seite – alle vier Rechtstext-Links vorhanden'); }); // P-01b: Rechtliches über Einstellungen (Mobile/PWA-Erreichbarkeit) async function gotoAuthenticated(page, path) { await page.goto(path); await page.waitForLoadState('networkidle'); // After full-page reload the app re-checks auth from localStorage; wait for spinner to clear await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 15000 }); } test('P-01b: Einstellungen enthält Link zu Rechtliches', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/settings'); await expect(page.locator('.app-main').getByRole('heading', { level: 1, name: 'Einstellungen' })).toBeVisible({ timeout: 8000 }); const link = page.locator('.app-main a[href="/settings/legal"]'); await expect(link).toBeVisible(); console.log('✓ P-01b: Einstellungen enthält Link zu /settings/legal'); }); test('P-01b: /settings/legal enthält Links zu allen vier Rechtstextseiten', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/settings/legal'); await expect(page.locator('.app-main').getByRole('heading', { level: 1, name: 'Rechtliches' })).toBeVisible({ timeout: 8000 }); for (const route of LEGAL_ROUTES) { const link = page.locator(`.app-main a[href="${route.path}"]`); await expect(link).toBeVisible(); } console.log('✓ P-01b: /settings/legal – Überschrift + alle vier Rechtstext-Links vorhanden'); }); test('P-01b: Jeder Rechtstext-Link aus /settings/legal führt zur korrekten Route', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); for (const route of LEGAL_ROUTES) { await gotoAuthenticated(page, '/settings/legal'); await expect(page.locator('.app-main').getByRole('heading', { level: 1, name: 'Rechtliches' })).toBeVisible({ timeout: 8000 }); await page.locator(`.app-main a[href="${route.path}"]`).click(); await page.waitForLoadState('networkidle'); expect(page.url()).toContain(route.path); await expect(page.getByRole('heading', { level: 1 })).toContainText(route.label); console.log(`✓ P-01b: ${route.label} – Link aus /settings/legal korrekt`); } }); // P-01c: Admin-konfigurierbare Rechtstexte test('P-01c: Rechtstextseiten zeigen Platzhalter-Banner wenn kein published-Dokument', async ({ page }) => { // Keine Auth — public route await page.goto('/impressum'); await page.waitForLoadState('networkidle'); // Seite erreichbar (kein Redirect zu /login) expect(page.url()).toContain('/impressum'); // Spinner weg, dann entweder Platzhalter-Banner oder API-Inhalt (beides OK) await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await expect(page.getByRole('heading', { level: 1 })).toBeVisible({ timeout: 5000 }); console.log('✓ P-01c: /impressum lädt ohne Fehler (API-fetch mit Fallback)'); }); test('P-01c: /admin/legal-documents erreichbar für Superadmin', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/admin/legal-documents'); // Superadmin sieht die Seite; normaler Admin landet auf 403 (PlatformAdminRoute) // Test läuft mit Superadmin-Account (TEST_EMAIL), also Seite sichtbar await expect(page.locator('.app-main').getByRole('heading', { level: 1, name: 'Rechtstexte verwalten' })).toBeVisible({ timeout: 8000 }); await page.screenshot({ path: 'screenshots/p01c-admin-legal-documents.png' }); console.log('✓ P-01c: /admin/legal-documents erreichbar, Überschrift sichtbar'); }); test('P-01c: Admin-Nav enthält Link zu Rechtstexten', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/admin/hierarchy'); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); const link = page.locator('a[href="/admin/legal-documents"]'); await expect(link).toBeVisible({ timeout: 5000 }); console.log('✓ P-01c: Admin-Nav enthält Link /admin/legal-documents'); }); // ── P-06: Upload-Einwilligungsdialog ──────────────────────────────────────── test('P-06a: Medienbibliothek lädt ohne Fehler', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/media'); await expect(page.locator('h1')).toContainText('Medienbibliothek', { timeout: 8000 }); await page.screenshot({ path: 'screenshots/p06a-media-library.png' }); console.log('✓ P-06a: Medienbibliothek erreichbar'); }); test('P-06b: Rechte-Dialog erscheint bei Dateiauswahl', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/media'); await expect(page.locator('h1')).toContainText('Medienbibliothek', { timeout: 8000 }); // Datei-Upload simulieren (ohne echte Datei – Datei-Input finden und Datei setzen) const fileInput = page.locator('input[type="file"]').first(); await fileInput.setInputFiles({ name: 'test.png', mimeType: 'image/png', buffer: Buffer.from('PNG-Testinhalt'), }); // Rechte-Dialog muss erscheinen await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); await expect(page.locator('[role="dialog"]')).toContainText('Rechte-Erklärung'); await expect(page.locator('[role="dialog"]')).toContainText('VORLÄUFIG'); await page.screenshot({ path: 'screenshots/p06b-rights-dialog.png' }); console.log('✓ P-06b: Rechte-Dialog erscheint bei Dateiauswahl'); }); test('P-06c: Dialog-Abbrechen bricht Upload ab', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/media'); await expect(page.locator('h1')).toContainText('Medienbibliothek', { timeout: 8000 }); const fileInput = page.locator('input[type="file"]').first(); await fileInput.setInputFiles({ name: 'test.png', mimeType: 'image/png', buffer: Buffer.from('PNG-Testinhalt'), }); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); await page.locator('[role="dialog"] button:has-text("Abbrechen")').click(); // Dialog geschlossen, kein Upload-Fortschrittsbalken await expect(page.locator('[role="dialog"]')).toHaveCount(0, { timeout: 3000 }); await page.screenshot({ path: 'screenshots/p06c-dialog-cancel.png' }); console.log('✓ P-06c: Dialog-Abbrechen schließt Dialog ohne Upload'); }); test('P-06d: Dialog-Bestätigung ohne Pflichtfelder zeigt Fehler', async ({ page }) => { await login(page); await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); await gotoAuthenticated(page, '/media'); await expect(page.locator('h1')).toContainText('Medienbibliothek', { timeout: 8000 }); const fileInput = page.locator('input[type="file"]').first(); await fileInput.setInputFiles({ name: 'test.png', mimeType: 'image/png', buffer: Buffer.from('PNG-Testinhalt'), }); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); // Ohne Felder ausfüllen direkt bestätigen await page.locator('[role="dialog"] button:has-text("Bestätigen")').click(); // Fehlermeldung im Dialog const dialog = page.locator('[role="dialog"]'); await expect(dialog).toBeVisible({ timeout: 2000 }); // Fehlermeldung muss sichtbar sein await expect(dialog.locator('p[style*="danger"], p[style*="color: var(--danger)"]')).toBeVisible({ timeout: 2000 }); await page.screenshot({ path: 'screenshots/p06d-dialog-validation.png' }); console.log('✓ P-06d: Dialog zeigt Fehler ohne Pflichtfelder'); }); test('P-06e: API-Endpoint /api/admin/media-rights/legacy-summary erreichbar (Superadmin)', async ({ request }) => { // Superadmin-Login via API const loginRes = await request.post('/api/auth/login', { data: { email: TEST_EMAIL, password: TEST_PASSWORD }, }); if (!loginRes.ok()) { console.log('⚠ P-06e: Login fehlgeschlagen – Test übersprungen'); return; } const loginData = await loginRes.json(); const token = loginData.token; if (!token) { console.log('⚠ P-06e: Kein Token – Test übersprungen'); return; } const res = await request.get('/api/admin/media-rights/legacy-summary', { headers: { 'X-Auth-Token': token }, }); // Endpoint existiert (200 oder 403 wenn kein Superadmin — aber nicht 404/500) expect([200, 403]).toContain(res.status()); if (res.status() === 200) { const data = await res.json(); expect(data).toHaveProperty('total_active_assets'); expect(data).toHaveProperty('legacy_unreviewed'); console.log(`✓ P-06e: Legacy-Summary: ${data.legacy_unreviewed} von ${data.total_active_assets} Medien`); } else { console.log('✓ P-06e: Endpoint existiert (403 erwartet für Nicht-Superadmin)'); } }); test('9. Keine kritischen Console-Fehler', async ({ page }) => { const errors = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); await login(page); await page.waitForLoadState('networkidle'); // Filtere unkritische Fehler const kritisch = errors.filter(e => !e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404') && !e.includes('vite.svg') ); if (kritisch.length > 0) { console.log('⚠ Console-Fehler:', kritisch.join(', ')); } else { console.log('✓ Keine kritischen Console-Fehler'); } expect(kritisch.length).toBe(0); });