test: stabilize Playwright smoke and issue-audit against dev
Some checks failed
Deploy Development / deploy (push) Successful in 1m11s
Build Test / pytest-backend (push) Failing after 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 22s

Fix nav selectors (Erfassen), shared auth via global-setup to avoid login rate limits, and desktop sidebar viewport handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-25 16:37:36 +02:00
parent 79ec249bc3
commit ae7d0ac1d9
6 changed files with 123 additions and 54 deletions

3
.gitignore vendored
View File

@ -58,6 +58,9 @@ coverage/
# Temp
tmp/
*.tmp
test-results/
screenshots/
tests/.auth/
# Claude: nur ausgewählte Bereiche versionieren (siehe .claude/rules/DOCUMENTATION.md)
.claude/**

View File

@ -1,12 +1,15 @@
module.exports = {
testDir: './tests',
timeout: 30000,
workers: 1,
globalSetup: require.resolve('./tests/global-setup.js'),
use: {
channel: 'chrome',
headless: true,
viewport: { width: 390, height: 844 },
screenshot: 'only-on-failure',
baseURL: 'https://dev.mitai.jinkendo.de',
storageState: './tests/.auth/user.json',
},
reporter: 'list',
};

View File

@ -1,65 +1,57 @@
const { test, expect } = require('@playwright/test');
const { loginViaForm, clickBottomNav } = require('./helpers');
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
test.describe('Login-Flow', () => {
test.use({ storageState: { cookies: [], origins: [] } });
async function login(page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
}
test('1. Login funktioniert', async ({ page }) => {
await page.goto('/');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
const loginButton = page.locator('button:has-text("Anmelden")');
await expect(loginButton).toHaveCount(0, { timeout: 10000 });
await page.screenshot({ path: 'screenshots/01-nach-login.png' });
console.log('Login erfolgreich');
test('1. Login funktioniert', async ({ page }) => {
await loginViaForm(page);
await page.screenshot({ path: 'screenshots/01-nach-login.png' });
console.log('Login erfolgreich');
});
});
test('2. Dashboard laedt ohne Fehler', async ({ page }) => {
await login(page);
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 });
await page.screenshot({ path: 'screenshots/02-dashboard.png' });
console.log('Dashboard OK');
});
test('3. Erfassung erreichbar', async ({ page }) => {
await login(page);
await page.click('text=Erfassung');
test('3. Erfassen erreichbar', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await clickBottomNav(page, 'Erfassen');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/\/capture/);
await page.screenshot({ path: 'screenshots/03-erfassung.png' });
console.log('Erfassung OK');
console.log('Erfassen OK');
});
test('4. Analyse erreichbar', async ({ page }) => {
await login(page);
await page.click('text=Analyse');
await page.goto('/');
await page.waitForLoadState('networkidle');
await clickBottomNav(page, 'Analyse');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/\/analysis/);
await page.screenshot({ path: 'screenshots/04-analyse.png' });
console.log('Analyse OK');
});
test('5. Keine kritischen Console-Fehler', async ({ page }) => {
const errors = [];
page.on('console', msg => {
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
await login(page);
await page.goto('/');
await page.waitForLoadState('networkidle');
const kritisch = errors.filter(e =>
!e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404')
const kritisch = errors.filter(
(e) => !e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404')
);
if (kritisch.length > 0) {
console.log('Console-Fehler:', kritisch.join(', '));
} else {
console.log('Keine kritischen Console-Fehler');
}
});
});

64
tests/global-setup.js Normal file
View File

@ -0,0 +1,64 @@
const { chromium } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
const AUTH_FILE = path.join(__dirname, '.auth', 'user.json');
async function tryLogin(page, baseURL) {
await page.goto(`${baseURL}/`);
await page.waitForLoadState('networkidle');
const loginBtn = page.locator('button:has-text("Anmelden")');
if ((await loginBtn.count()) === 0) {
return { ok: true, status: 200 };
}
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
const responsePromise = page
.waitForResponse(
(r) => r.url().includes('/api/auth/login') && r.request().method() === 'POST',
{ timeout: 15000 }
)
.catch(() => null);
await page.click('button:has-text("Anmelden")');
const response = await responsePromise;
await page.waitForLoadState('networkidle');
const loginVisible = await page.locator('button:has-text("Anmelden")').count();
return { ok: loginVisible === 0, status: response?.status() ?? null };
}
module.exports = async (config) => {
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
const browser = await chromium.launch({ channel: 'chrome', headless: true });
const page = await browser.newPage();
const baseURL = config.projects?.[0]?.use?.baseURL || 'https://dev.mitai.jinkendo.de';
const waits = [0, 15000, 30000, 45000];
let lastStatus = null;
for (let i = 0; i < waits.length; i++) {
if (waits[i] > 0) {
console.log(`Global setup: warte ${waits[i] / 1000}s (Login-Rate-Limit 5/min)…`);
await page.waitForTimeout(waits[i]);
}
const result = await tryLogin(page, baseURL);
lastStatus = result.status;
if (result.ok) {
await page.context().storageState({ path: AUTH_FILE });
await browser.close();
return;
}
}
await browser.close();
throw new Error(
`Global setup: Login fehlgeschlagen (letzter HTTP-Status: ${lastStatus ?? 'unbekannt'})`
);
};

23
tests/helpers.js Normal file
View File

@ -0,0 +1,23 @@
const { expect } = require('@playwright/test');
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
/** Nutzt storageState aus global-setup; nur für Tests nötig, die explizit Login prüfen. */
async function loginViaForm(page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
await expect(page.locator('button:has-text("Anmelden")')).toHaveCount(0, { timeout: 15000 });
}
async function clickBottomNav(page, label) {
const link = page.locator('nav.bottom-nav a.nav-item').filter({ hasText: label });
await expect(link).toBeVisible({ timeout: 10000 });
await link.click();
}
module.exports = { TEST_EMAIL, TEST_PASSWORD, loginViaForm, clickBottomNav };

View File

@ -4,28 +4,15 @@
*/
const { test, expect } = require('@playwright/test');
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
async function login(page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
await expect(page.locator('button:has-text("Anmelden")')).toHaveCount(0, { timeout: 15000 });
}
test.describe('Issue-Audit UI', () => {
test('#40 Logout-Button im Mobile-Header sichtbar', async ({ page }) => {
await login(page);
await page.goto('/');
await page.waitForLoadState('networkidle');
const logoutBtn = page.locator('header.app-header--mobile button[title="Abmelden"]');
await expect(logoutBtn).toBeVisible();
});
test('#25 Goals-Seite erreichbar und rendert Inhalt', async ({ page }) => {
await login(page);
await page.goto('/goals');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).not.toContainText('404', { timeout: 5000 });
@ -36,7 +23,6 @@ test.describe('Issue-Audit UI', () => {
});
test('#65 Dashboard-Layout-Konfiguration erreichbar', async ({ page }) => {
await login(page);
await page.goto('/settings/dashboard-layout');
await page.waitForLoadState('networkidle');
const body = await page.locator('body').innerText();
@ -44,21 +30,19 @@ test.describe('Issue-Audit UI', () => {
});
test('#30 Desktop-Sidebar bei breitem Viewport', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.setViewportSize({ width: 1280, height: 800 });
await login(page);
const sidebar = page.locator('.desktop-sidebar, [class*="desktop-sidebar"]');
await expect(sidebar.first()).toBeVisible({ timeout: 10000 });
await expect(page.locator('aside.desktop-sidebar')).toBeVisible({ timeout: 10000 });
});
test('#38 Nutrition CSV Import — FDDB-Panel mit Usage-Integration', async ({ page }) => {
await login(page);
await page.goto('/nutrition');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: /Import/i }).click();
await page.waitForLoadState('networkidle');
const importCard = page.locator('.card').filter({ hasText: 'FDDB CSV Import' });
await expect(importCard).toBeVisible();
// Nach Deploy: badge-container-right am Titel; UsageBadge nur bei endlichem Limit (Premium = kein Badge)
const titleRow = importCard.locator('.card-title.badge-container-right');
if (await titleRow.count()) {
await expect(titleRow).toBeVisible();