test: stabilize Playwright smoke and issue-audit against dev
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:
parent
79ec249bc3
commit
ae7d0ac1d9
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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/**
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,61 +1,53 @@
|
|||
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';
|
||||
|
||||
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.describe('Login-Flow', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
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 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(', '));
|
||||
|
|
|
|||
64
tests/global-setup.js
Normal file
64
tests/global-setup.js
Normal 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
23
tests/helpers.js
Normal 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 };
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user