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

272 lines
12 KiB
TypeScript

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<string, string> = { '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<number> {
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<void>, 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();
});