/** * 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 = { '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(); }); });