shinkan-jinkendo/tests/dev-smoke-test.spec.js
Lars e4e362b0a9
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m30s
chore(version): update version and changelog for release 0.8.126
- Bumped APP_VERSION to 0.8.126 and updated the changelog to reflect recent changes.
- Added the TrainingPlanningFrameworkImportModal component to the TrainingPlanningPage for improved training session management.
- Implemented a new Playwright test to verify the functionality of the framework import dialog in the training planning page.
2026-05-14 13:26:02 +02:00

647 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: <h1>Vereinsverwaltung</h1> + Tab <h2>Vereine</h2> → 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('13. Trainingsplanung: Rahmen-Import-Dialog öffnet und schließt', async ({ page }, testInfo) => {
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,
});
const openBtn = main.getByRole('button', { name: /Aus Rahmen übernehmen/i });
if (await openBtn.isDisabled()) {
testInfo.skip(true, 'Keine Trainingsgruppe — Button bleibt deaktiviert');
return;
}
await openBtn.click();
const dlg = page.getByTestId('planning-framework-import-modal');
await expect(dlg).toBeVisible({ timeout: 10000 });
await expect(dlg.getByRole('heading', { name: /Sessions aus Rahmen übernehmen/i })).toBeVisible();
await dlg.getByRole('button', { name: 'Abbrechen' }).click();
await expect(dlg).toHaveCount(0);
console.log('✓ Trainingsplanung: Rahmen-Import-Dialog Smoke');
});
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);
});