# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: dev-smoke-test.spec.js >> 8. Dashboard API-Budget nach Reload (profiles/me, dashboard/kpis) - Location: tests\dev-smoke-test.spec.js:165:1 # Error details ``` Error: page.goto: net::ERR_CONNECTION_REFUSED at http://127.0.0.1:3098/ Call log: - navigating to "http://127.0.0.1:3098/", waiting until "load" ``` # Test source ```ts 1 | const { test, expect } = require('@playwright/test'); 2 | 3 | const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com'; 4 | const TEST_PASSWORD = process.env.TEST_PASSWORD || '12345678'; 5 | 6 | /** Primärer Submit auf der Login-Seite (nicht den Tab "Login" vs. "Registrieren"). */ 7 | async function submitLoginForm(page) { 8 | await page.getByRole('button', { name: 'Anmelden' }).click(); 9 | } 10 | 11 | async function login(page) { > 12 | await page.goto('/'); | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://127.0.0.1:3098/ 13 | await page.waitForLoadState('networkidle'); 14 | 15 | // Warte bis Login-Seite geladen ist 16 | await page.waitForSelector('input[type="email"]', { timeout: 10000 }); 17 | 18 | await page.fill('input[type="email"]', TEST_EMAIL); 19 | await page.fill('input[type="password"]', TEST_PASSWORD); 20 | await submitLoginForm(page); 21 | // Wait until auth is complete: URL leaves /login and Dashboard is rendered 22 | await page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 15000 }); 23 | await page.waitForLoadState('networkidle'); 24 | } 25 | 26 | test('1. Login funktioniert', async ({ page }) => { 27 | await page.goto('/'); 28 | await page.waitForSelector('input[type="email"]', { timeout: 10000 }); 29 | await page.fill('input[type="email"]', TEST_EMAIL); 30 | await page.fill('input[type="password"]', TEST_PASSWORD); 31 | await submitLoginForm(page); 32 | await page.waitForLoadState('networkidle'); 33 | 34 | // Nach Login soll der Tab "Login" (Moduswahl) verschwinden — nicht der Submit "Anmelden" 35 | const loginButton = page.locator('button:has-text("Login")'); 36 | await expect(loginButton).toHaveCount(0, { timeout: 10000 }); 37 | 38 | await page.screenshot({ path: 'screenshots/01-nach-login.png' }); 39 | console.log('✓ Login erfolgreich'); 40 | }); 41 | 42 | test('2. Dashboard lädt ohne Fehler', async ({ page }) => { 43 | await login(page); 44 | 45 | // Warte bis Spinner verschwunden 46 | await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); 47 | 48 | // Dashboard: h1 „Dashboard“ + Begrüßungstext (nicht mehr „Willkommen bei Shinkan“ als Überschrift) 49 | const main = page.locator('.app-main'); 50 | await expect(main.getByRole('heading', { level: 1, name: 'Dashboard' })).toBeVisible({ 51 | timeout: 5000, 52 | }); 53 | await expect(main.getByText(/Shinkan unterstützt dich/i)).toBeVisible({ timeout: 5000 }); 54 | 55 | await page.screenshot({ path: 'screenshots/02-dashboard.png' }); 56 | console.log('✓ Dashboard OK'); 57 | }); 58 | 59 | test('3. Navigation zu Übungen', async ({ page }) => { 60 | await login(page); 61 | 62 | await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 }); 63 | 64 | // Bei Viewport ≥1024px ist .bottom-nav versteckt — Mobile garantieren wie in playwright.config.js 65 | await page.setViewportSize({ width: 390, height: 844 }); 66 | 67 | // Bottom-Nav: Navigation und URL gemeinsam abwarten (vermeidet race mit networkidle) 68 | const exercisesLink = page.locator('.bottom-nav').getByRole('link', { name: /Übungen/i }); 69 | await Promise.all([ 70 | page.waitForURL( 71 | (u) => { 72 | const path = u.pathname.replace(/\/$/, '') || '/' 73 | return path === '/exercises' 74 | }, 75 | { timeout: 15000 }, 76 | ), 77 | exercisesLink.click(), 78 | ]); 79 | await page.waitForLoadState('networkidle'); 80 | 81 | // Wie Test 4 (Vereine): eine eindeutige h1 — nicht h1,h2-Kombi (Strict Mode + mehrere Treffer) 82 | const main = page.locator('.app-main'); 83 | await expect(main.getByRole('heading', { level: 1, name: /Übungen/i })).toBeVisible({ 84 | timeout: 10000, 85 | }); 86 | 87 | await page.screenshot({ path: 'screenshots/03-uebungen.png' }); 88 | console.log('✓ Übungen-Seite erreichbar'); 89 | }); 90 | 91 | test('4. Navigation zu Vereine', async ({ page }) => { 92 | await login(page); 93 | await page.setViewportSize({ width: 390, height: 844 }); 94 | 95 | await page.locator('.bottom-nav a[href="/clubs"]').click(); 96 | await page.waitForLoadState('networkidle'); 97 | 98 | // ClubsPage:

Vereinsverwaltung

+ Tab

Vereine

→ ein kombinierter 99 | // Selektor löst 2 Treffer aus (Playwright strict mode). URL + primäre Überschrift reichen. 100 | await expect(page).toHaveURL(/\/clubs\/?$/, { timeout: 5000 }); 101 | await expect(page.getByRole('heading', { level: 1, name: /Vereinsverwaltung/i })).toBeVisible({ 102 | timeout: 5000, 103 | }); 104 | 105 | await page.screenshot({ path: 'screenshots/04-vereine.png' }); 106 | console.log('✓ Vereine-Seite erreichbar'); 107 | }); 108 | 109 | test('5. Desktop-Sidebar sichtbar (Desktop)', async ({ page }) => { 110 | // Desktop-Viewport 111 | await page.setViewportSize({ width: 1280, height: 800 }); 112 | ```