diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 35e31d2..a46e062 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -15,6 +15,9 @@ jobs: - name: Install backend dependencies working-directory: backend run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + - name: Typecheck backend + working-directory: backend + run: npx tsc --noEmit - name: Run backend tests working-directory: backend run: npm test @@ -30,6 +33,9 @@ jobs: - name: Install frontend dependencies working-directory: frontend run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + - name: Typecheck frontend + working-directory: frontend + run: npx tsc --noEmit - name: Run frontend tests working-directory: frontend run: npm test diff --git a/frontend/playwright-tests/perf-gate.test.ts b/frontend/playwright-tests/perf-gate.test.ts new file mode 100644 index 0000000..1f6e42e --- /dev/null +++ b/frontend/playwright-tests/perf-gate.test.ts @@ -0,0 +1,90 @@ +/** + * Performance Gate (Task I1): 10.000 Interaktions-Budget. + * Erstellt ein Projekt mit Elementen, misst die Zeit fuer 10.000 + * Canvas-Interaktionen ( dispatched pointer events ) und prueft + * dass sie innerhalb des Budgets liegen und keine JS-Fehler auftreten. + */ +import { test, expect, chromium } from '@playwright/test'; + +const API_URL = process.env.API_URL || 'http://localhost:3001'; +const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173'; +const N_INTERACTIONS = 10000; +const BUDGET_MS = 15000; // 10k Interaktionen muessen in < 15s sein + +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, + }); + return { status: res.status, body: await res.json() }; +} + +test('Perf Gate: 10k Canvas-Interaktionen im Budget, keine JS-Fehler', async () => { + test.setTimeout(180000); + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); + const jsErrors: string[] = []; + page.on('pageerror', (err) => jsErrors.push(String(err))); + + const email = `perf-${Date.now()}@example.com`; + const reg = await api('POST', '/api/auth/register', { email, password: 'Test1234!', name: 'Perf' }); + expect(reg.status).toBe(201); + const token = reg.body.session.token; + + const proj = await api('POST', '/api/projects', { name: `Perf-${Date.now()}`, description: 'perf gate' }, token); + expect(proj.status).toBe(201); + + 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: 30000 }); + await page.locator('.dashboard-project-card').first().click(); + await page.waitForSelector('canvas', { timeout: 30000 }); + await page.waitForTimeout(2000); // Engines mounten + + // Finde den Haupt-Canvas + const canvas = page.locator('canvas').first(); + const box = await canvas.boundingBox(); + expect(box).toBeTruthy(); + + // ── 10.000 Interaktionen ueber dispatched events (schnell, im Browser) ── + const elapsed = await page.evaluate(({ n, bx, by }) => { + const target = document.querySelector('canvas') as HTMLCanvasElement; + const startX = bx + 100; + const startY = by + 100; + const t0 = performance.now(); + let x = startX, y = startY; + for (let i = 0; i < n; i++) { + x = startX + Math.sin(i * 0.01) * 300; + y = startY + Math.cos(i * 0.013) * 200; + target.dispatchEvent(new PointerEvent('pointermove', { + clientX: x, clientY: y, button: 0, buttons: 0, + pointerId: 1, pointerType: 'mouse', + bubbles: true, cancelable: true, + })); + if (i % 500 === 0) { + // Alle 500 auch ein down/up-Paar (Interaktionsmix) + target.dispatchEvent(new PointerEvent('pointerdown', { + clientX: x, clientY: y, button: 0, buttons: 1, + pointerId: 1, pointerType: 'mouse', + bubbles: true, cancelable: true, + })); + target.dispatchEvent(new PointerEvent('pointerup', { + clientX: x, clientY: y, button: 0, buttons: 0, + pointerId: 1, pointerType: 'mouse', + bubbles: true, cancelable: true, + })); + } + } + return performance.now() - t0; + }, { n: N_INTERACTIONS, bx: box!.x, by: box!.y }); + + console.log(`Perf Gate: ${N_INTERACTIONS} Interaktionen in ${elapsed.toFixed(0)}ms (Budget: ${BUDGET_MS}ms)`); + expect(elapsed).toBeLessThan(BUDGET_MS); + expect(jsErrors).toHaveLength(0); + + await browser.close(); +});