# 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 >> 11. Übungsliste: Massenauswahl zeigt Bulk-Toolbar
- Location: tests\dev-smoke-test.spec.js:253: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: <h1>Vereinsverwaltung</h1> + Tab <h2>Vereine</h2> → 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 | 
```