2026-07-27 02:02:35 +02:00
|
|
|
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();
|
2026-07-27 03:15:30 +02:00
|
|
|
// Card click now opens the project directly
|
2026-07-27 02:02:35 +02:00
|
|
|
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();
|
|
|
|
|
});
|