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
|
# Temp
|
||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
|
test-results/
|
||||||
|
screenshots/
|
||||||
|
tests/.auth/
|
||||||
|
|
||||||
# Claude: nur ausgewählte Bereiche versionieren (siehe .claude/rules/DOCUMENTATION.md)
|
# Claude: nur ausgewählte Bereiche versionieren (siehe .claude/rules/DOCUMENTATION.md)
|
||||||
.claude/**
|
.claude/**
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
workers: 1,
|
||||||
|
globalSetup: require.resolve('./tests/global-setup.js'),
|
||||||
use: {
|
use: {
|
||||||
channel: 'chrome',
|
channel: 'chrome',
|
||||||
headless: true,
|
headless: true,
|
||||||
viewport: { width: 390, height: 844 },
|
viewport: { width: 390, height: 844 },
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
baseURL: 'https://dev.mitai.jinkendo.de',
|
baseURL: 'https://dev.mitai.jinkendo.de',
|
||||||
|
storageState: './tests/.auth/user.json',
|
||||||
},
|
},
|
||||||
reporter: 'list',
|
reporter: 'list',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,53 @@
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const { loginViaForm, clickBottomNav } = require('./helpers');
|
||||||
|
|
||||||
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
|
test.describe('Login-Flow', () => {
|
||||||
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
async function login(page) {
|
test('1. Login funktioniert', async ({ page }) => {
|
||||||
await page.goto('/');
|
await loginViaForm(page);
|
||||||
await page.waitForLoadState('networkidle');
|
await page.screenshot({ path: 'screenshots/01-nach-login.png' });
|
||||||
await page.fill('input[type="email"]', TEST_EMAIL);
|
console.log('Login erfolgreich');
|
||||||
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('2. Dashboard laedt ohne Fehler', async ({ page }) => {
|
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 expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 });
|
||||||
await page.screenshot({ path: 'screenshots/02-dashboard.png' });
|
await page.screenshot({ path: 'screenshots/02-dashboard.png' });
|
||||||
console.log('Dashboard OK');
|
console.log('Dashboard OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('3. Erfassung erreichbar', async ({ page }) => {
|
test('3. Erfassen erreichbar', async ({ page }) => {
|
||||||
await login(page);
|
await page.goto('/');
|
||||||
await page.click('text=Erfassung');
|
|
||||||
await page.waitForLoadState('networkidle');
|
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' });
|
await page.screenshot({ path: 'screenshots/03-erfassung.png' });
|
||||||
console.log('Erfassung OK');
|
console.log('Erfassen OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('4. Analyse erreichbar', async ({ page }) => {
|
test('4. Analyse erreichbar', async ({ page }) => {
|
||||||
await login(page);
|
await page.goto('/');
|
||||||
await page.click('text=Analyse');
|
|
||||||
await page.waitForLoadState('networkidle');
|
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' });
|
await page.screenshot({ path: 'screenshots/04-analyse.png' });
|
||||||
console.log('Analyse OK');
|
console.log('Analyse OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('5. Keine kritischen Console-Fehler', async ({ page }) => {
|
test('5. Keine kritischen Console-Fehler', async ({ page }) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('console', msg => {
|
page.on('console', (msg) => {
|
||||||
if (msg.type() === 'error') errors.push(msg.text());
|
if (msg.type() === 'error') errors.push(msg.text());
|
||||||
});
|
});
|
||||||
await login(page);
|
await page.goto('/');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
const kritisch = errors.filter(e =>
|
const kritisch = errors.filter(
|
||||||
!e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404')
|
(e) => !e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404')
|
||||||
);
|
);
|
||||||
if (kritisch.length > 0) {
|
if (kritisch.length > 0) {
|
||||||
console.log('Console-Fehler:', kritisch.join(', '));
|
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, 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.describe('Issue-Audit UI', () => {
|
||||||
test('#40 Logout-Button im Mobile-Header sichtbar', async ({ page }) => {
|
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"]');
|
const logoutBtn = page.locator('header.app-header--mobile button[title="Abmelden"]');
|
||||||
await expect(logoutBtn).toBeVisible();
|
await expect(logoutBtn).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#25 Goals-Seite erreichbar und rendert Inhalt', async ({ page }) => {
|
test('#25 Goals-Seite erreichbar und rendert Inhalt', async ({ page }) => {
|
||||||
await login(page);
|
|
||||||
await page.goto('/goals');
|
await page.goto('/goals');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await expect(page.locator('body')).not.toContainText('404', { timeout: 5000 });
|
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 }) => {
|
test('#65 Dashboard-Layout-Konfiguration erreichbar', async ({ page }) => {
|
||||||
await login(page);
|
|
||||||
await page.goto('/settings/dashboard-layout');
|
await page.goto('/settings/dashboard-layout');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
const body = await page.locator('body').innerText();
|
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 }) => {
|
test('#30 Desktop-Sidebar bei breitem Viewport', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
await page.setViewportSize({ width: 1280, height: 800 });
|
await page.setViewportSize({ width: 1280, height: 800 });
|
||||||
await login(page);
|
await expect(page.locator('aside.desktop-sidebar')).toBeVisible({ timeout: 10000 });
|
||||||
const sidebar = page.locator('.desktop-sidebar, [class*="desktop-sidebar"]');
|
|
||||||
await expect(sidebar.first()).toBeVisible({ timeout: 10000 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#38 Nutrition CSV Import — FDDB-Panel mit Usage-Integration', async ({ page }) => {
|
test('#38 Nutrition CSV Import — FDDB-Panel mit Usage-Integration', async ({ page }) => {
|
||||||
await login(page);
|
|
||||||
await page.goto('/nutrition');
|
await page.goto('/nutrition');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByRole('button', { name: /Import/i }).click();
|
await page.getByRole('button', { name: /Import/i }).click();
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
const importCard = page.locator('.card').filter({ hasText: 'FDDB CSV Import' });
|
const importCard = page.locator('.card').filter({ hasText: 'FDDB CSV Import' });
|
||||||
await expect(importCard).toBeVisible();
|
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');
|
const titleRow = importCard.locator('.card-title.badge-container-right');
|
||||||
if (await titleRow.count()) {
|
if (await titleRow.count()) {
|
||||||
await expect(titleRow).toBeVisible();
|
await expect(titleRow).toBeVisible();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user