import { test, expect, chromium, Page } from '@playwright/test'; const API_URL = process.env.API_URL || 'http://localhost:3001'; const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173'; 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 url = `${API_URL}${endpoint}`; for (let attempt = 0; attempt < 3; attempt++) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, signal: controller.signal }); clearTimeout(timeout); const text = await res.text(); let json: any = null; try { json = text ? JSON.parse(text) : null; } catch {} return { status: res.status, body: json }; } catch (e) { if (attempt === 2) throw e; await new Promise(r => setTimeout(r, 500)); } } throw new Error('unreachable'); } class ToolTester { page: Page; token: string; projectId: string; drawingId: string; cx: number; cy: number; report: Array<{ tool: string; category: string; pass: boolean; note: string; before: number; after: number }> = []; constructor(page: Page, token: string, projectId: string, drawingId: string, cx: number, cy: number) { this.page = page; this.token = token; this.projectId = projectId; this.drawingId = drawingId; this.cx = cx; this.cy = cy; } async getCount(): Promise { const r = await api('GET', `/api/drawings/${this.drawingId}/elements`, undefined, this.token); return r.body?.length ?? 0; } async clickTool(tool: string) { const btn = this.page.locator(`[data-tool="${tool}"]`).first(); await btn.scrollIntoViewIfNeeded(); await btn.click(); await this.page.waitForTimeout(250); } async esc() { await this.page.keyboard.press('Escape'); await this.page.waitForTimeout(200); } async click(x: number, y: number) { await this.page.mouse.click(x, y); await this.page.waitForTimeout(300); } async drag(x1: number, y1: number, x2: number, y2: number) { await this.page.mouse.move(x1, y1); await this.page.mouse.down(); await this.page.mouse.move(x2, y2, { steps: 5 }); await this.page.mouse.up(); await this.page.waitForTimeout(500); } async reset() { try { await this.clickTool('select'); await this.esc(); } catch {} await this.page.waitForTimeout(200); } async run(tool: string, category: string, action: () => Promise, expectChange?: 'increase' | 'decrease' | 'same' | 'any') { let before = 0, after = 0, pass = false, note = ''; try { await this.reset(); before = await this.getCount(); await this.clickTool(tool); await action(); await this.page.waitForTimeout(700); after = await this.getCount(); if (expectChange === 'increase') pass = after > before; else if (expectChange === 'decrease') pass = after < before; else if (expectChange === 'same') pass = after === before; else pass = true; note = `before=${before} after=${after}`; } catch (e: any) { pass = false; note = `ERROR: ${e.message}`; try { after = await this.getCount(); } catch {} } this.report.push({ tool, category, pass, note, before, after }); console.log(`${tool}: ${pass ? 'PASS' : 'FAIL'} - ${note}`); await this.reset(); } } test('exhaustive tool test v2', async () => { test.setTimeout(600000); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); const logs: string[] = []; page.on('console', msg => logs.push(`${msg.type()}: ${msg.text()}`)); page.on('pageerror', err => logs.push(`PAGEERROR: ${err.message}`)); const email = `exhaustive-v2-${Date.now()}@example.com`; const reg = await api('POST', '/api/auth/register', { email, password: 'Test1234!', name: 'Exhaustive V2 Bot' }); const token = reg.body.session.token; const proj = await api('POST', '/api/projects', { name: 'Exhaustive V2', description: 'x' }, token); const projectId = proj.body.id; await page.goto(`${BASE_URL}/`); await page.evaluate((t: string) => localStorage.setItem('auth_token', t), token); await page.reload(); await page.waitForSelector('.dashboard-project-card', { timeout: 10000 }); await page.locator('.dashboard-project-card').first().click(); // Card click now opens the project directly await page.waitForSelector('canvas', { timeout: 10000 }); await page.waitForTimeout(2500); let drawingId: string | null = null; for (let i = 0; i < 30; i++) { const ds = await api('GET', `/api/projects/${projectId}/drawings`, undefined, token); if (ds.body?.length) { drawingId = ds.body[0].id; break; } await new Promise(r => setTimeout(r, 300)); } if (!drawingId) throw new Error('No drawing created'); const canvas = page.locator('canvas').first(); const box = await canvas.boundingBox(); if (!box) throw new Error('Canvas not found'); const cx = box.x + box.width / 2; const cy = box.y + box.height / 2; const T = new ToolTester(page, token, projectId, drawingId, cx, cy); // --- Zeichnen --- await T.run('line', 'Zeichnen', async () => { await T.drag(cx - 100, cy, cx + 100, cy); }, 'increase'); await T.run('rect', 'Zeichnen', async () => { await T.drag(cx - 120, cy - 80, cx + 120, cy + 80); }, 'increase'); await T.run('circle', 'Zeichnen', async () => { await T.drag(cx, cy - 50, cx, cy + 50); }, 'increase'); await T.run('arc', 'Zeichnen', async () => { await T.click(cx, cy); await T.click(cx + 60, cy); await T.click(cx, cy + 60); }, 'increase'); await T.run('polyline', 'Zeichnen', async () => { await T.click(cx - 80, cy - 80); await T.click(cx, cy - 80); await T.click(cx + 80, cy - 80); await page.keyboard.press('Enter'); }, 'increase'); await T.run('polygon', 'Zeichnen', async () => { await T.click(cx - 70, cy + 70); await T.click(cx, cy + 140); await T.click(cx + 70, cy + 70); await page.keyboard.press('Enter'); }, 'increase'); await T.run('dimension', 'Zeichnen', async () => { await T.drag(cx - 100, cy - 200, cx + 100, cy - 200); }, 'increase'); await T.run('leader', 'Zeichnen', async () => { await T.drag(cx - 150, cy - 150, cx - 80, cy - 220); }, 'increase'); await T.run('revcloud', 'Zeichnen', async () => { await T.click(cx - 50, cy + 150); await T.click(cx + 50, cy + 150); await T.click(cx + 50, cy + 250); await T.click(cx - 50, cy + 250); await page.keyboard.press('Enter'); }, 'increase'); // --- Bestuhlung --- await T.run('chair', 'Bestuhlung', async () => { await T.click(cx - 200, cy - 200); }, 'increase'); await T.run('table', 'Bestuhlung', async () => { await T.click(cx + 200, cy - 200); }, 'increase'); await T.run('stage', 'Bestuhlung', async () => { await T.click(cx + 200, cy + 200); }, 'increase'); await T.run('seating-row', 'Bestuhlung', async () => { await T.click(cx - 200, cy + 200); }, 'increase'); await T.run('seating-block', 'Bestuhlung', async () => { await T.click(cx - 250, cy + 100); }, 'increase'); await T.run('seating-template', 'Bestuhlung', async () => { const sel = page.locator('select.template-dropdown').first(); await sel.selectOption({ index: 1 }); await page.waitForTimeout(400); await T.click(cx + 250, cy + 100); }, 'increase'); // --- Text --- await T.run('text', 'Zeichnen', async () => { await T.click(cx, cy + 300); await page.waitForTimeout(300); const editor = page.locator('[contenteditable="true"]').first(); if (await editor.isVisible().catch(() => false)) { await editor.fill('Testtext'); await page.keyboard.press('Enter'); } await T.esc(); }, 'increase'); // --- Hatch (needs closed shape) --- await T.run('hatch', 'Zeichnen', async () => { // ensure a rect in center const cnt = await T.getCount(); if (cnt === 0) { await T.clickTool('rect'); await T.drag(cx - 50, cy - 50, cx + 50, cy + 50); await T.clickTool('hatch'); } await T.click(cx, cy); }, 'any'); // --- Measure --- await T.run('measure', 'Anzeige', async () => { await T.click(cx - 50, cy - 50); await T.click(cx + 50, cy + 50); }, 'same'); // --- Bearbeiten: create base line --- await T.clickTool('line'); await T.drag(cx - 100, cy + 150, cx + 100, cy + 150); await T.reset(); await T.run('move', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx, cy + 150); await T.clickTool('move'); await T.drag(cx, cy + 150, cx + 50, cy + 200); }, 'same'); await T.run('copy', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx + 50, cy + 200); await T.clickTool('copy'); await T.drag(cx + 50, cy + 200, cx + 50, cy + 250); }, 'increase'); await T.run('rotate', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx + 50, cy + 250); await T.clickTool('rotate'); await T.drag(cx + 50, cy + 250, cx + 100, cy + 250); }, 'same'); await T.run('scale', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx + 50, cy + 250); await T.clickTool('scale'); await T.drag(cx + 50, cy + 250, cx + 120, cy + 250); }, 'same'); await T.run('mirror', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx + 50, cy + 250); await T.clickTool('mirror'); await T.click(cx - 80, cy + 120); await T.click(cx - 80, cy + 280); }, 'same'); await T.run('offset', 'Bearbeiten', async () => { await T.clickTool('select'); await T.click(cx + 50, cy + 250); await T.clickTool('offset'); await T.click(cx + 50, cy + 250); await T.click(cx + 120, cy + 250); }, 'increase'); // Draw a dedicated line to delete, then verify deletion await T.clickTool('line'); await T.drag(cx - 80, cy + 140, cx + 80, cy + 140); await page.waitForTimeout(1500); await T.run('delete', 'Bearbeiten', async () => { await T.clickTool('delete'); await T.click(cx, cy + 140); }, 'decrease'); // --- Trim/Extend/Fillet --- await T.clickTool('line'); await T.drag(cx - 100, cy + 220, cx + 100, cy + 220); await T.clickTool('line'); await T.drag(cx, cy + 170, cx, cy + 270); await T.reset(); await T.run('trim', 'Bearbeiten', async () => { await T.clickTool('trim'); await T.click(cx, cy + 220); await T.click(cx, cy + 190); }, 'any'); // recreate crossing lines if needed if (await T.getCount() < 2) { await T.clickTool('line'); await T.drag(cx - 100, cy + 220, cx + 100, cy + 220); await T.clickTool('line'); await T.drag(cx, cy + 170, cx, cy + 270); await T.reset(); } await T.run('extend', 'Bearbeiten', async () => { await T.clickTool('extend'); await T.click(cx, cy + 220); await T.click(cx + 120, cy + 220); }, 'any'); // Fillet corner await T.clickTool('line'); await T.drag(cx - 80, cy + 120, cx - 80, cy + 180); await T.clickTool('line'); await T.drag(cx - 80, cy + 180, cx + 20, cy + 180); await T.reset(); await T.run('fillet', 'Bearbeiten', async () => { await T.clickTool('fillet'); await T.click(cx - 80, cy + 150); await T.click(cx - 30, cy + 180); }, 'any'); // --- Pan / Select smoke --- await T.run('pan', 'Anzeige', async () => { await page.mouse.move(cx, cy); await page.mouse.down({ button: 'middle' }); await page.mouse.move(cx + 50, cy + 50); await page.mouse.up({ button: 'middle' }); }, 'same'); await T.run('select', 'Anzeige', async () => { await T.click(cx, cy); }, 'same'); await page.evaluate((r: any) => { (window as any).__toolReport = r; }, T.report); console.log('\n=== SUMMARY ==='); const fails = T.report.filter(r => !r.pass); console.log(`Total: ${T.report.length}, Passed: ${T.report.length - fails.length}, Failed: ${fails.length}`); fails.forEach(f => console.log(`FAIL: ${f.tool} (${f.category}) - ${f.note}`)); expect(fails.length).toBe(0); await browser.close(); });