task(I1): CI performance gate (10k interaction budget), typecheck steps for both suites
This commit is contained in:
@@ -15,6 +15,9 @@ jobs:
|
|||||||
- name: Install backend dependencies
|
- name: Install backend dependencies
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps
|
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
|
- name: Run backend tests
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: npm test
|
run: npm test
|
||||||
@@ -30,6 +33,9 @@ jobs:
|
|||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps
|
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
|
- name: Run frontend tests
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|||||||
@@ -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<string, string> = { '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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user