Files
web-cad/frontend/playwright-tests/manual/browser-exhaustive.test.ts
T

348 lines
14 KiB
TypeScript

/**
* Exhaustive Browser Test Suite for Web CAD
* Tests every tool, menu, dialog and workflow reachable in the UI.
* Run with: npx playwright test playwright-tests/browser-exhaustive.test.ts --project=chromium
*/
import { test, expect, chromium, type Browser, type Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
const API_URL = process.env.API_URL || 'http://localhost:3001';
const TEST_EMAIL = `browser-test-${Date.now()}@example.com`;
const TEST_PASSWORD = 'Test1234!';
interface Issue {
category: string;
severity: 'critical' | 'major' | 'minor' | 'info';
title: string;
detail: string;
screenshot?: string;
}
const issues: Issue[] = [];
async function api(method: string, endpoint: string, body?: unknown, token?: string) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${API_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
return { status: res.status, body: text ? JSON.parse(text) : null };
}
function logIssue(category: string, severity: Issue['severity'], title: string, detail: string, screenshot?: string) {
issues.push({ category, severity, title, detail, screenshot });
console.log(`[${severity.toUpperCase()}] ${category}: ${title}`);
}
async function screenshot(page: Page, name: string) {
const file = path.resolve(`/tmp/web-cad-clone/frontend/playwright-tests/screenshots/${name}.png`);
await page.screenshot({ path: file, fullPage: false });
return file;
}
async function captureConsoleErrors(page: Page, label: string) {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
const text = msg.text();
// Ignore benign 404s for settings that don't exist yet
if (text.includes('/api/settings/')) return;
errors.push(text);
logIssue('Console', 'major', `Console error during ${label}`, text);
}
});
page.on('pageerror', err => {
const text = err.message;
errors.push(text);
logIssue('PageError', 'critical', `Page error during ${label}`, text);
});
return () => errors;
}
async function waitForStable(page: Page, ms = 300) {
await page.waitForTimeout(ms);
}
async function clickIfExists(page: Page, selector: string, label: string) {
const el = await page.locator(selector).first();
if (await el.count() > 0 && await el.isVisible().catch(() => false)) {
await el.click();
return true;
}
logIssue('UI', 'minor', `${label} not found`, `Selector ${selector} not visible`);
return false;
}
test.describe('Web CAD Exhaustive Browser Tests', () => {
test.setTimeout(300000);
let browser: Browser;
let page: Page;
let token: string;
let userId: string;
let projectId: string;
test.beforeAll(async () => {
// Ensure screenshot dir exists
fs.mkdirSync('/tmp/web-cad-clone/frontend/playwright-tests/screenshots', { recursive: true });
browser = await chromium.launch({ headless: true });
page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
// Register test user
const reg = await api('POST', '/api/auth/register', { email: TEST_EMAIL, password: TEST_PASSWORD, name: 'Browser Bot' });
if (reg.status !== 200 && reg.status !== 201) {
// Maybe user already exists? Try login
const login = await api('POST', '/api/auth/login', { email: TEST_EMAIL, password: TEST_PASSWORD });
if (login.status !== 200) throw new Error(`Auth failed: ${login.status} ${JSON.stringify(login.body)}`);
token = login.body.session.token;
userId = login.body.user.id;
} else {
token = reg.body.session.token;
userId = reg.body.user.id;
}
// Create project
const proj = await api('POST', '/api/projects', { name: 'Exhaustive Browser Test', description: 'Automated test' }, token);
if (proj.status !== 200 && proj.status !== 201) throw new Error(`Project create failed: ${proj.status} ${JSON.stringify(proj.body)}`);
projectId = proj.body.id;
// Inject session and open app
await page.goto(`${BASE_URL}/`);
await page.evaluate((t) => { localStorage.setItem('auth_token', t); }, token);
await page.reload();
await page.waitForSelector('.dashboard-project-card', { timeout: 10000 });
});
test.afterAll(async () => {
await browser.close();
// Write JSON report
const report = {
totalIssues: issues.length,
critical: issues.filter(i => i.severity === 'critical').length,
major: issues.filter(i => i.severity === 'major').length,
minor: issues.filter(i => i.severity === 'minor').length,
info: issues.filter(i => i.severity === 'info').length,
issues,
};
fs.writeFileSync('/tmp/web-cad-clone/frontend/playwright-tests/browser-test-report.json', JSON.stringify(report, null, 2));
console.log('\n=== REPORT ===');
console.log(JSON.stringify(report, null, 2));
});
test('01: open project and load CAD editor', async () => {
const stopCapture = captureConsoleErrors(page, 'open project');
await page.locator('.dashboard-project-card').first().click();
// Card click now opens the project directly
try {
await page.waitForSelector('[data-tool="line"]', { timeout: 10000 });
await waitForStable(page, 1000);
} catch (e) {
logIssue('Editor', 'critical', 'CAD editor did not render tool buttons', String(e), await screenshot(page, '01_editor_not_loaded'));
}
stopCapture();
});
test('02: draw tools - line, rect, circle', async () => {
const stopCapture = captureConsoleErrors(page, 'draw tools');
for (const tool of ['line', 'rect', 'circle']) {
const clicked = await clickIfExists(page, `[data-tool="${tool}"]`, `${tool} tool button`);
if (!clicked) {
// Try alternative selectors
const alt = await page.locator('.tool-button, .ribbon-button, button').filter({ hasText: new RegExp(tool, 'i') }).first();
if (await alt.count() > 0 && await alt.isVisible().catch(() => false)) {
await alt.click();
} else {
logIssue('DrawTool', 'major', `Cannot select ${tool} tool`, 'Tool button not found');
continue;
}
}
await waitForStable(page, 300);
// Draw on canvas center (two clicks for 2-point tools)
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) {
logIssue('DrawTool', 'critical', `Canvas not found for ${tool}`, '');
continue;
}
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
await page.mouse.click(cx - 50, cy - 50);
await waitForStable(page, 200);
await page.mouse.click(cx + 50, cy + 50);
await waitForStable(page, 500);
await page.keyboard.press('Escape');
await waitForStable(page, 200);
}
stopCapture();
});
test('03: modification tools - select, move, copy, delete', async () => {
const stopCapture = captureConsoleErrors(page, 'modification tools');
await clickIfExists(page, '[data-tool="select"], button:has-text("select")', 'select tool');
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) {
logIssue('ModifyTool', 'critical', 'Canvas not found', '');
return;
}
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
// Select element at center
await page.mouse.click(cx, cy);
await waitForStable(page, 300);
// Try move tool and drag
await clickIfExists(page, '[data-tool="move"], button:has-text("move")', 'move tool');
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 100, cy + 100);
await page.mouse.up();
await waitForStable(page, 500);
// Copy tool
await clickIfExists(page, '[data-tool="copy"], button:has-text("copy")', 'copy tool');
await page.mouse.move(cx + 100, cy + 100);
await page.mouse.down();
await page.mouse.move(cx + 200, cy + 200);
await page.mouse.up();
await waitForStable(page, 500);
// Delete tool
await clickIfExists(page, '[data-tool="delete"], button:has-text("delete")', 'delete tool');
await page.mouse.click(cx, cy);
await waitForStable(page, 300);
stopCapture();
});
test('04: seating tools - row, block, table, stage', async () => {
const stopCapture = captureConsoleErrors(page, 'seating tools');
for (const tool of ['seating-row', 'seating-block', 'table', 'stage']) {
const clicked = await clickIfExists(page, `[data-tool="${tool}"]`, `${tool} tool`);
if (!clicked) {
logIssue('SeatingTool', 'major', `Cannot select ${tool} tool`, 'Tool button not found');
continue;
}
await waitForStable(page, 300);
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) continue;
const cx = box.x + box.width / 2 + (Math.random() * 200 - 100);
const cy = box.y + box.height / 2 + (Math.random() * 200 - 100);
await page.mouse.click(cx, cy);
await waitForStable(page, 500);
await page.keyboard.press('Escape');
await waitForStable(page, 200);
}
stopCapture();
});
test('05: layer panel operations', async () => {
const stopCapture = captureConsoleErrors(page, 'layer panel');
const opened = await clickIfExists(page, '[data-testid="layer-panel-toggle"], button:has-text("Layers")', 'layer panel toggle');
if (opened) {
await clickIfExists(page, 'button:has-text("+ Layer")', 'add layer button');
await waitForStable(page, 500);
await clickIfExists(page, 'button:has-text("+ Layer")', 'add layer button second');
}
stopCapture();
});
test('06: block library', async () => {
const stopCapture = captureConsoleErrors(page, 'block library');
await clickIfExists(page, '[data-testid="block-library-toggle"], button:has-text("Blocks")', 'block library toggle');
await waitForStable(page, 500);
stopCapture();
});
test('07: topbar menus and ribbon', async () => {
const stopCapture = captureConsoleErrors(page, 'topbar ribbon');
// Try clicking each ribbon tab
const tabs = ['Start', 'Zeichnen', 'Bearbeiten', 'Ansicht', 'Bestuhlung', 'Einfügen'];
for (const tab of tabs) {
await clickIfExists(page, `.ribbon-tab:has-text("${tab}"), button:has-text("${tab}")`, `${tab} ribbon tab`);
await waitForStable(page, 300);
}
stopCapture();
});
test('08: settings modal', async () => {
const stopCapture = captureConsoleErrors(page, 'settings');
await clickIfExists(page, 'button[aria-label="Einstellungen"], .settings-button, button:has-text("⚙")', 'settings button');
await waitForStable(page, 500);
await clickIfExists(page, 'button:has-text("Schließen"), button:has-text("Close"), .modal-close', 'close settings');
stopCapture();
});
test('09: notifications', async () => {
const stopCapture = captureConsoleErrors(page, 'notifications');
await clickIfExists(page, 'button[aria-label="Benachrichtigungen"], .notifications-button', 'notifications button');
await waitForStable(page, 500);
await page.keyboard.press('Escape');
stopCapture();
});
test('10: AI copilot panel', async () => {
const stopCapture = captureConsoleErrors(page, 'ai copilot');
await clickIfExists(page, '[data-testid="ki-copilot-toggle"], button:has-text("KI"), button:has-text("Copilot")', 'KI copilot toggle');
await waitForStable(page, 500);
const input = await page.locator('.copilot-input, input[placeholder*="KI"]').first();
if (await input.count() > 0 && await input.isVisible().catch(() => false)) {
await input.fill('Erstelle einen Tisch mit 8 Stühlen');
await input.press('Enter');
await waitForStable(page, 1000);
} else {
logIssue('AICopilot', 'minor', 'KI Copilot input not found', '');
}
stopCapture();
});
test('11: zoom and pan controls', async () => {
const stopCapture = captureConsoleErrors(page, 'zoom pan');
await clickIfExists(page, 'button:has-text("+")', 'zoom in');
await clickIfExists(page, 'button:has-text("-")', 'zoom out');
await clickIfExists(page, 'button:has-text("Fit")', 'fit view');
stopCapture();
});
test('12: command line', async () => {
const stopCapture = captureConsoleErrors(page, 'command line');
const cmd = await page.locator('.command-line-input, input[placeholder*="Befehl"]').first();
if (await cmd.count() > 0 && await cmd.isVisible().catch(() => false)) {
await cmd.fill('line');
await cmd.press('Enter');
await waitForStable(page, 500);
}
stopCapture();
});
test('13: undo/redo shortcuts', async () => {
const stopCapture = captureConsoleErrors(page, 'undo redo');
await page.keyboard.press('Control+z');
await waitForStable(page, 300);
await page.keyboard.press('Control+y');
await waitForStable(page, 300);
stopCapture();
});
test('14: export buttons', async () => {
const stopCapture = captureConsoleErrors(page, 'export');
await clickIfExists(page, 'button:has-text("Export"), button:has-text("DXF"), button:has-text("PDF")', 'export buttons');
await waitForStable(page, 500);
stopCapture();
});
test('15: logout returns to login', async () => {
const stopCapture = captureConsoleErrors(page, 'logout');
await clickIfExists(page, 'button:has-text("Abmelden"), button:has-text("Logout")', 'logout button');
await waitForStable(page, 1000);
const loginVisible = await page.locator('text=Anmelden').first().isVisible().catch(() => false);
if (!loginVisible) {
logIssue('Auth', 'major', 'Logout did not return to login screen', '', await screenshot(page, '15_logout_failed'));
}
stopCapture();
});
});