chore(version): update version and changelog for release 0.8.125
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
- Bumped APP_VERSION to 0.8.125 and updated the changelog to reflect recent changes. - Added new tests for the dashboard API to ensure proper HTTP 200 responses when inner lists are mocked. - Enhanced the ExerciseListBulkToolbar component with a data-testid for improved testing capabilities. - Refactored the TrainingPlanningPage by extracting utility functions to trainingPlanningPageHelpers for better code organization.
This commit is contained in:
parent
1631bd2e02
commit
300d916fad
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
|
@ -11,6 +12,7 @@ os.environ.setdefault("SKIP_DB_MIGRATE", "1")
|
||||||
|
|
||||||
from fastapi_param_unwrap import unwrap_query_default
|
from fastapi_param_unwrap import unwrap_query_default
|
||||||
from main import app
|
from main import app
|
||||||
|
from tenant_context import TenantContext, get_tenant_context
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -27,3 +29,41 @@ def test_unwrap_query_default_for_direct_route_calls() -> None:
|
||||||
def test_dashboard_kpis_unauthenticated_401(client: TestClient) -> None:
|
def test_dashboard_kpis_unauthenticated_401(client: TestClient) -> None:
|
||||||
r = client.get("/api/dashboard/kpis")
|
r = client.get("/api/dashboard/kpis")
|
||||||
assert r.status_code == 401
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_tenant_for_kpis() -> TenantContext:
|
||||||
|
return TenantContext(
|
||||||
|
profile_id=42,
|
||||||
|
global_role="trainer",
|
||||||
|
effective_club_id=7,
|
||||||
|
club_ids=frozenset({7}),
|
||||||
|
memberships=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("routers.dashboard.list_training_units")
|
||||||
|
@patch("routers.dashboard.list_exercises_like_get")
|
||||||
|
def test_dashboard_kpis_200_when_inner_lists_mocked(
|
||||||
|
mock_list_ex: object,
|
||||||
|
mock_list_tu: object,
|
||||||
|
client: TestClient,
|
||||||
|
) -> None:
|
||||||
|
mock_list_ex.return_value = []
|
||||||
|
mock_list_tu.return_value = []
|
||||||
|
app.dependency_overrides[get_tenant_context] = _fake_tenant_for_kpis
|
||||||
|
try:
|
||||||
|
r = client.get("/api/dashboard/kpis")
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
data = r.json()
|
||||||
|
assert "year" in data
|
||||||
|
assert data["draft_count"] == 0
|
||||||
|
assert data["mine_count"] == 0
|
||||||
|
assert data["ytd_completed_count"] == 0
|
||||||
|
th = data["training_home"]
|
||||||
|
assert th["upcoming"] == []
|
||||||
|
assert th["planned_with_notes"] == []
|
||||||
|
assert th["review_pending"] == []
|
||||||
|
assert mock_list_ex.call_count == 2
|
||||||
|
assert mock_list_tu.call_count == 3
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.124"
|
APP_VERSION = "0.8.125"
|
||||||
BUILD_DATE = "2026-05-12"
|
BUILD_DATE = "2026-05-12"
|
||||||
DB_SCHEMA_VERSION = "20260514062"
|
DB_SCHEMA_VERSION = "20260514062"
|
||||||
|
|
||||||
|
|
@ -36,6 +36,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.125",
|
||||||
|
"date": "2026-05-13",
|
||||||
|
"changes": [
|
||||||
|
"Tests: Playwright 11 (Übungsliste Bulk-Toolbar), 12 (Trainingsplanung); Dashboard-Test 8 prüft HTTP 200 auf /api/dashboard/kpis; pytest test_dashboard_kpis_200_when_inner_lists_mocked.",
|
||||||
|
"Frontend Phase 3: trainingPlanningPageHelpers.js aus TrainingPlanningPage; ExerciseListBulkToolbar data-testid.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.124",
|
"version": "0.8.124",
|
||||||
"date": "2026-05-13",
|
"date": "2026-05-13",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function ExerciseListBulkToolbar({
|
||||||
if (selectedCount < 1) return null
|
if (selectedCount < 1) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card exercise-bulk-toolbar">
|
<div className="card exercise-bulk-toolbar" data-testid="exercise-list-bulk-toolbar">
|
||||||
<strong>{selectedCount} ausgewählt</strong>
|
<strong>{selectedCount} ausgewählt</strong>
|
||||||
<button type="button" className="btn btn-secondary btn-small" onClick={onClearSelection}>
|
<button type="button" className="btn btn-secondary btn-small" onClick={onClearSelection}>
|
||||||
Auswahl aufheben
|
Auswahl aufheben
|
||||||
|
|
|
||||||
|
|
@ -17,112 +17,22 @@ import {
|
||||||
hydrateExercisePlanningRow,
|
hydrateExercisePlanningRow,
|
||||||
insertTrainingModuleIntoPlanningSections,
|
insertTrainingModuleIntoPlanningSections,
|
||||||
} from '../utils/trainingUnitSectionsForm'
|
} from '../utils/trainingUnitSectionsForm'
|
||||||
|
import {
|
||||||
|
trainingVisibilityShortDE,
|
||||||
|
addDaysIsoDate,
|
||||||
|
pad2,
|
||||||
|
toIsoLocal,
|
||||||
|
mondayIndex,
|
||||||
|
getCalendarGridRange,
|
||||||
|
shiftCalendarMonth,
|
||||||
|
enumerateIsoDays,
|
||||||
|
WEEKDAYS_DE,
|
||||||
|
toNumList,
|
||||||
|
sessionAssignDefaults,
|
||||||
|
normalizeGroupCoTrainerIds,
|
||||||
|
filterDirectoryExcludingLead,
|
||||||
|
} from '../utils/trainingPlanningPageHelpers'
|
||||||
|
|
||||||
/** Kurz-Anzeige Sichtbarkeit (Trainingsmodule, Übungen) */
|
|
||||||
function trainingVisibilityShortDE(visibility) {
|
|
||||||
const v = String(visibility || '').trim().toLowerCase()
|
|
||||||
if (v === 'official') return 'Öffentliche Bibliothek'
|
|
||||||
if (v === 'club') return 'Verein'
|
|
||||||
if (v === 'private') return 'Privat'
|
|
||||||
return visibility ? String(visibility) : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDaysIsoDate(isoDay, daysDelta) {
|
|
||||||
const d = new Date(`${isoDay}T12:00:00`)
|
|
||||||
d.setDate(d.getDate() + daysDelta)
|
|
||||||
return d.toISOString().slice(0, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pad2(n) {
|
|
||||||
return String(n).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
function toIsoLocal(d) {
|
|
||||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Montag = erster Wochentag (ISO-Woche UI) */
|
|
||||||
function mondayIndex(d) {
|
|
||||||
return (d.getDay() + 6) % 7
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Kalendarische Monatsansicht: erster und letzter Tag des sichtbaren Rasters (Mo–So) */
|
|
||||||
function getCalendarGridRange(ym) {
|
|
||||||
const parts = (ym || '').split('-').map(Number)
|
|
||||||
const y = parts[0]
|
|
||||||
const m = parts[1]
|
|
||||||
if (!y || !m || m < 1 || m > 12) {
|
|
||||||
const t = new Date()
|
|
||||||
return { gridStart: toIsoLocal(t), gridEnd: toIsoLocal(t) }
|
|
||||||
}
|
|
||||||
const first = new Date(y, m - 1, 1)
|
|
||||||
const last = new Date(y, m, 0)
|
|
||||||
const gridStart = new Date(first)
|
|
||||||
gridStart.setDate(first.getDate() - mondayIndex(first))
|
|
||||||
const lastMon = mondayIndex(last)
|
|
||||||
const gridEnd = new Date(last)
|
|
||||||
gridEnd.setDate(last.getDate() + (6 - lastMon))
|
|
||||||
return { gridStart: toIsoLocal(gridStart), gridEnd: toIsoLocal(gridEnd) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function shiftCalendarMonth(ym, delta) {
|
|
||||||
const parts = (ym || '').split('-').map(Number)
|
|
||||||
const y = parts[0] || new Date().getFullYear()
|
|
||||||
const m = parts[1] || 1
|
|
||||||
const d = new Date(y, m - 1 + delta, 1)
|
|
||||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function enumerateIsoDays(fromIso, toIso) {
|
|
||||||
const out = []
|
|
||||||
const cur = new Date(`${fromIso}T12:00:00`)
|
|
||||||
const end = new Date(`${toIso}T12:00:00`)
|
|
||||||
while (cur <= end) {
|
|
||||||
out.push(toIsoLocal(cur))
|
|
||||||
cur.setDate(cur.getDate() + 1)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
|
||||||
|
|
||||||
function toNumList(arr) {
|
|
||||||
if (!Array.isArray(arr)) return []
|
|
||||||
const out = []
|
|
||||||
for (const x of arr) {
|
|
||||||
const n = Number(x)
|
|
||||||
if (Number.isFinite(n) && n >= 1) out.push(n)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionAssignDefaults = () => ({
|
|
||||||
lead_trainer_profile_id: '',
|
|
||||||
session_assistants_inherit: true,
|
|
||||||
session_assistant_profile_ids: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Co_trainer_ids aus TrainingGroups (Liste/JSON) → Zahlenliste */
|
|
||||||
function normalizeGroupCoTrainerIds(raw) {
|
|
||||||
if (raw == null) return []
|
|
||||||
const arr = Array.isArray(raw) ? raw : []
|
|
||||||
const out = []
|
|
||||||
for (const x of arr) {
|
|
||||||
const n = Number(x)
|
|
||||||
if (Number.isFinite(n) && n >= 1) out.push(n)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mitgliederverzeichnis-Einträge ohne effektiven Leitungsträger als Co‑Option */
|
|
||||||
function filterDirectoryExcludingLead(directory, excludeLeadPid) {
|
|
||||||
const ex =
|
|
||||||
excludeLeadPid != null && excludeLeadPid !== '' && Number.isFinite(Number(excludeLeadPid))
|
|
||||||
? Number(excludeLeadPid)
|
|
||||||
: null
|
|
||||||
if (ex == null) return directory
|
|
||||||
return directory.filter((m) => Number(m.id) !== ex)
|
|
||||||
}
|
|
||||||
function TrainingPlanningPage() {
|
function TrainingPlanningPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
|
||||||
106
frontend/src/utils/trainingPlanningPageHelpers.js
Normal file
106
frontend/src/utils/trainingPlanningPageHelpers.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
/** Reine Hilfen für Trainingsplanung (Kalender, Sichtbarkeits-Kurztext, Trainer-Zuordnung). */
|
||||||
|
|
||||||
|
export function trainingVisibilityShortDE(visibility) {
|
||||||
|
const v = String(visibility || '').trim().toLowerCase()
|
||||||
|
if (v === 'official') return 'Öffentliche Bibliothek'
|
||||||
|
if (v === 'club') return 'Verein'
|
||||||
|
if (v === 'private') return 'Privat'
|
||||||
|
return visibility ? String(visibility) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDaysIsoDate(isoDay, daysDelta) {
|
||||||
|
const d = new Date(`${isoDay}T12:00:00`)
|
||||||
|
d.setDate(d.getDate() + daysDelta)
|
||||||
|
return d.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pad2(n) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toIsoLocal(d) {
|
||||||
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Montag = erster Wochentag (ISO-Woche UI) */
|
||||||
|
export function mondayIndex(d) {
|
||||||
|
return (d.getDay() + 6) % 7
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kalendarische Monatsansicht: erster und letzter Tag des sichtbaren Rasters (Mo–So) */
|
||||||
|
export function getCalendarGridRange(ym) {
|
||||||
|
const parts = (ym || '').split('-').map(Number)
|
||||||
|
const y = parts[0]
|
||||||
|
const m = parts[1]
|
||||||
|
if (!y || !m || m < 1 || m > 12) {
|
||||||
|
const t = new Date()
|
||||||
|
return { gridStart: toIsoLocal(t), gridEnd: toIsoLocal(t) }
|
||||||
|
}
|
||||||
|
const first = new Date(y, m - 1, 1)
|
||||||
|
const last = new Date(y, m, 0)
|
||||||
|
const gridStart = new Date(first)
|
||||||
|
gridStart.setDate(first.getDate() - mondayIndex(first))
|
||||||
|
const lastMon = mondayIndex(last)
|
||||||
|
const gridEnd = new Date(last)
|
||||||
|
gridEnd.setDate(last.getDate() + (6 - lastMon))
|
||||||
|
return { gridStart: toIsoLocal(gridStart), gridEnd: toIsoLocal(gridEnd) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftCalendarMonth(ym, delta) {
|
||||||
|
const parts = (ym || '').split('-').map(Number)
|
||||||
|
const y = parts[0] || new Date().getFullYear()
|
||||||
|
const m = parts[1] || 1
|
||||||
|
const d = new Date(y, m - 1 + delta, 1)
|
||||||
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enumerateIsoDays(fromIso, toIso) {
|
||||||
|
const out = []
|
||||||
|
const cur = new Date(`${fromIso}T12:00:00`)
|
||||||
|
const end = new Date(`${toIso}T12:00:00`)
|
||||||
|
while (cur <= end) {
|
||||||
|
out.push(toIsoLocal(cur))
|
||||||
|
cur.setDate(cur.getDate() + 1)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||||
|
|
||||||
|
export function toNumList(arr) {
|
||||||
|
if (!Array.isArray(arr)) return []
|
||||||
|
const out = []
|
||||||
|
for (const x of arr) {
|
||||||
|
const n = Number(x)
|
||||||
|
if (Number.isFinite(n) && n >= 1) out.push(n)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionAssignDefaults = () => ({
|
||||||
|
lead_trainer_profile_id: '',
|
||||||
|
session_assistants_inherit: true,
|
||||||
|
session_assistant_profile_ids: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Co_trainer_ids aus TrainingGroups (Liste/JSON) → Zahlenliste */
|
||||||
|
export function normalizeGroupCoTrainerIds(raw) {
|
||||||
|
if (raw == null) return []
|
||||||
|
const arr = Array.isArray(raw) ? raw : []
|
||||||
|
const out = []
|
||||||
|
for (const x of arr) {
|
||||||
|
const n = Number(x)
|
||||||
|
if (Number.isFinite(n) && n >= 1) out.push(n)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mitgliederverzeichnis-Einträge ohne effektiven Leitungsträger als Co‑Option */
|
||||||
|
export function filterDirectoryExcludingLead(directory, excludeLeadPid) {
|
||||||
|
const ex =
|
||||||
|
excludeLeadPid != null && excludeLeadPid !== '' && Number.isFinite(Number(excludeLeadPid))
|
||||||
|
? Number(excludeLeadPid)
|
||||||
|
: null
|
||||||
|
if (ex == null) return directory
|
||||||
|
return directory.filter((m) => Number(m.id) !== ex)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
"status": "passed",
|
"status": "failed",
|
||||||
"failedTests": []
|
"failedTests": [
|
||||||
|
"d6ae548bbe32e0652471-c2435d34f500841a9fcc",
|
||||||
|
"d6ae548bbe32e0652471-6495823d1677ce34da5c",
|
||||||
|
"d6ae548bbe32e0652471-b581d2777c999619d7af"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
# 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 |
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -0,0 +1,137 @@
|
||||||
|
# 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 >> 12. Trainingsplanung: Seite lädt mit Überschrift
|
||||||
|
- Location: tests\dev-smoke-test.spec.js:275: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 |
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -0,0 +1,137 @@
|
||||||
|
# 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: <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 |
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -184,6 +184,17 @@ test('8. Dashboard API-Budget nach Reload (profiles/me, dashboard/kpis)', async
|
||||||
|
|
||||||
page.on('request', onRequest);
|
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 {
|
try {
|
||||||
await page.reload({ waitUntil: 'networkidle' });
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
|
@ -199,11 +210,13 @@ test('8. Dashboard API-Budget nach Reload (profiles/me, dashboard/kpis)', async
|
||||||
expect(profilesMe).toBe(1);
|
expect(profilesMe).toBe(1);
|
||||||
expect(trainingUnits).toBe(0);
|
expect(trainingUnits).toBe(0);
|
||||||
expect(dashboardKpis).toBe(1);
|
expect(dashboardKpis).toBe(1);
|
||||||
|
expect(kpisStatuses.some((s) => s === 200)).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
page.off('request', onRequest);
|
page.off('request', onRequest);
|
||||||
|
page.off('response', onResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✓ Dashboard API-Budget: 1× profiles/me, 0× training-units, 1× dashboard/kpis');
|
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 }) => {
|
test('9. Übungsliste: nach Laden entweder Treffer-Gitter oder Leerhinweis', async ({ page }) => {
|
||||||
|
|
@ -237,6 +250,39 @@ test('10. Übungsliste: Filter-Dialog öffnet und schließt', async ({ page }) =
|
||||||
console.log('✓ Übungsliste: Filter-Dialog Smoke');
|
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 }) => {
|
test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1280, height: 800 });
|
await page.setViewportSize({ width: 1280, height: 800 });
|
||||||
await login(page);
|
await login(page);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user