shinkan-jinkendo/tests/dev-smoke-test.spec.js
Lars 28ca64b5b4
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 31s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 26s
feat(compliance): P-12 sessionStorage-Bereinigung bei Logout (0.8.68)
Sicherheit P-12 (MITT-05): logout() entfernt alle sj_coach_*-Schlüssel
aus sessionStorage gezielt per Präfix-Löschung. Fremde Schlüssel
(Browser-Extensions etc.) bleiben erhalten. Verhindert Datenleak bei
Nutzerwechsel im selben Tab (geteilter Rechner).

- AuthContext.jsx: Präfix-Schleife in logout()
- tests/dev-smoke-test.spec.js: Playwright-Test P-12 (injects/checks 3
  sj_coach_*-Schlüssel + 1 Fremd-Schlüssel; prüft selektive Löschung)

Compliance-Dokumentation:
- docs/compliance-implementation.md: P-12 , Version 0.8.68
- docs/compliance-package-register.md: kanonisches Paketregister (neu)
- docs/compliance-roadmap.md: lebende Steuerungs-Roadmap (neu)
- docs/compliance-audit.md: §20 Paket-ID-Stabilitätsregel

version: 0.8.68 (backend + frontend)
module: auth 1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:08:28 +02:00

210 lines
7.6 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);
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);
// Bei Viewport ≥1024px ist .bottom-nav versteckt — Mobile garantieren wie in playwright.config.js
await page.setViewportSize({ width: 390, height: 844 });
// Desktop-Sidebar enthält ebenfalls Übungen nur Mobile-Bottom-Nav klicken (sichtbarer Link)
await page.locator('.bottom-nav a[href="/exercises"]').click();
await page.waitForLoadState('networkidle');
// Prüfe ob Übungen-Seite geladen
await expect(page.locator('h1, h2, .page-title')).toContainText(/übungen/i, { timeout: 5000 });
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');
});
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');
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');
});
test('8. 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);
});