2026-09-16 18:03:08 +02:00
|
|
|
/**
|
|
|
|
|
* CAD-14 Beweis-Test: Der Workflow, den der Nutzer jeden Tag macht.
|
|
|
|
|
* 1. Werkzeug waehlen (Sidebar), 2. zeichnen (2 Klicks),
|
|
|
|
|
* 3. EXAKT EIN Element an der geklickten Position erwarten.
|
|
|
|
|
* ROT-Zustand (jetzt): 2 Elemente (Legacy + V2 je eins) = Doppelung belegt.
|
|
|
|
|
* GRUEN nach Schnitt: 1 Element, Position = Klickposition.
|
|
|
|
|
*/
|
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
const BASE_URL = process.env.FRONTEND_URL || 'https://web-cad-neu.server.media-on.de';
|
2026-09-16 22:06:23 +02:00
|
|
|
const API_URL = process.env.API_URL || 'https://web-cad-neu.server.media-on.de';
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
|
|
|
}
|
2026-09-16 18:03:08 +02:00
|
|
|
|
|
|
|
|
interface VerifyWindow {
|
|
|
|
|
__v2Debug?: { info: () => { cachedElements: number; ready: boolean } | null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('zeichnen erzeugt GENAU EIN Element an der Klick-Position', async ({ page }) => {
|
2026-09-16 23:25:22 +02:00
|
|
|
test.setTimeout(120000);
|
2026-09-16 18:03:08 +02:00
|
|
|
await page.goto(BASE_URL);
|
|
|
|
|
await page.fill('input[type=email]', 'admin@media-on.de');
|
|
|
|
|
await page.fill('input[type=password]', 'WebCAD2026!');
|
|
|
|
|
await page.click('button[type=submit]');
|
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
|
|
2026-09-16 22:06:23 +02:00
|
|
|
// CAD-14: Frisches Projekt + Zeichnung -> Basis 0, Zuordnung eindeutig
|
|
|
|
|
const token = await page.evaluate(() => localStorage.getItem('auth_token'));
|
|
|
|
|
expect(token).toBeTruthy();
|
|
|
|
|
const uniq = Date.now();
|
|
|
|
|
const projRes = await fetch(`${BASE_URL}/api/projects`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
|
|
|
body: JSON.stringify({ name: `CAD14-${uniq}`, description: 'proof' }),
|
|
|
|
|
});
|
|
|
|
|
const proj = await projRes.json();
|
|
|
|
|
expect(proj?.id).toBeTruthy();
|
|
|
|
|
const drawingRes = await fetch(`${BASE_URL}/api/projects/${proj.id}/drawings`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
|
|
|
body: JSON.stringify({ name: `CAD14-Proof-${uniq}` }),
|
|
|
|
|
});
|
|
|
|
|
const drawing = await drawingRes.json();
|
|
|
|
|
expect(drawing?.id).toBeTruthy();
|
|
|
|
|
const drawingId: string = drawing.id;
|
|
|
|
|
const beforeDbCount = 0;
|
|
|
|
|
|
|
|
|
|
// Karte des frischen Projekts oeffnen (eindeutig per Name) — Dashboard
|
|
|
|
|
// laedt die Liste beim Mount, daher Reload nach API-Anlage.
|
|
|
|
|
await page.reload();
|
|
|
|
|
await page.waitForTimeout(2500);
|
|
|
|
|
const card = page.locator(`.dashboard-project-card, [class*=project-card]`).filter({ hasText: `CAD14-${uniq}` }).first();
|
|
|
|
|
await card.click();
|
2026-09-16 18:03:08 +02:00
|
|
|
await page.waitForTimeout(2500);
|
|
|
|
|
|
|
|
|
|
const verify = () => page.evaluate(() => {
|
|
|
|
|
const w = window as unknown as VerifyWindow;
|
|
|
|
|
return w.__v2Debug?.info?.() ?? null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const before = await verify();
|
|
|
|
|
const beforeCount = before?.cachedElements ?? -1;
|
|
|
|
|
expect(beforeCount, 'Pixi muss ready sein').toBeGreaterThanOrEqual(0);
|
|
|
|
|
|
2026-09-16 22:06:23 +02:00
|
|
|
// DB-Basis ist 0 (frische Zeichnung)
|
|
|
|
|
|
2026-09-16 18:03:08 +02:00
|
|
|
// Linie-Werkzeug aus der SIDEBAR (der richtige Ort)
|
|
|
|
|
await page.locator('[data-tool="line"]').first().click();
|
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
|
|
|
|
|
|
// Auf die Zeichenflaeche klicken: zwei Klicks = eine Linie
|
|
|
|
|
const canvas = page.locator('canvas').last();
|
|
|
|
|
const box = await canvas.boundingBox();
|
|
|
|
|
if (!box) throw new Error('Kein Canvas gefunden');
|
|
|
|
|
const cx = box.x + box.width / 2;
|
|
|
|
|
const cy = box.y + box.height / 2;
|
2026-09-16 22:06:23 +02:00
|
|
|
// CAD-14: line ist ein DRAG-Werkzeug (down -> move -> up committet).
|
|
|
|
|
const startX = cx - 60;
|
|
|
|
|
const startY = cy - 30;
|
|
|
|
|
await page.mouse.move(startX, startY);
|
|
|
|
|
await page.mouse.down();
|
|
|
|
|
await page.mouse.move(startX + 120, startY + 60, { steps: 8 });
|
|
|
|
|
await page.mouse.up();
|
|
|
|
|
await page.waitForTimeout(800);
|
2026-09-16 18:03:08 +02:00
|
|
|
|
|
|
|
|
const after = await verify();
|
|
|
|
|
const afterCount = after?.cachedElements ?? -1;
|
|
|
|
|
|
2026-09-16 22:06:23 +02:00
|
|
|
// ABNAHME 1: genau +1 Element im Renderer
|
2026-09-16 18:03:08 +02:00
|
|
|
expect(afterCount - beforeCount, `Erwartet genau 1 neues Element, got ${afterCount - beforeCount}`).toBe(1);
|
2026-09-16 22:06:23 +02:00
|
|
|
|
|
|
|
|
// ABNAHME 2: das Element MUSS in der DB persistiert sein (Autosave/Save-Kette)
|
|
|
|
|
// drawingId aus Scope (frisch angelegt)
|
|
|
|
|
// Autosave braucht bis zu 30s — poll bis zu 40s
|
|
|
|
|
// CAD-14: ALLE Drawings des Proof-Projekts pruefen (die App koennte in
|
|
|
|
|
// eine auto-angelegte Zeichnung schreiben statt in meine).
|
|
|
|
|
let dbCount = -1;
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
await page.waitForTimeout(5000);
|
|
|
|
|
const drs = await (await fetch(`${BASE_URL}/api/projects/${proj.id}/drawings`, { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const d of drs) {
|
2026-09-16 23:25:22 +02:00
|
|
|
const raw = await (await fetch(`${BASE_URL}/api/drawings/${d.id}/elements`, { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
|
|
|
total += raw.length;
|
|
|
|
|
// CAD-14: rohe DB-Zeile hat properties_json (String) — parse und
|
|
|
|
|
// melde Elemente mit chordCount (fuer die Options-Abnahme unten).
|
|
|
|
|
for (const e of raw) {
|
|
|
|
|
const parsed = typeof e.properties_json === 'string' ? JSON.parse(e.properties_json) : (e.properties_json ?? {});
|
|
|
|
|
if (parsed && parsed.chordCount !== undefined) {
|
|
|
|
|
console.log(`ELEMENT MIT CHORDCOUNT in ${d.id}:`, parsed.chordCount);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-16 22:06:23 +02:00
|
|
|
}
|
|
|
|
|
dbCount = total;
|
2026-09-16 23:25:22 +02:00
|
|
|
if (total >= 1) break;
|
2026-09-16 22:06:23 +02:00
|
|
|
}
|
|
|
|
|
expect(dbCount, `Genau 1 Element muss in der DB landen (vor ${beforeDbCount}, nach ${dbCount})`).toBe(1);
|
|
|
|
|
|
|
|
|
|
// ABNAHME 3: V2-Select-Tool klickt auf die Linienmitte — Auswahl muss
|
|
|
|
|
// auf dem V2-Dokument landen (Karte: __v2GetSelection).
|
|
|
|
|
await page.locator('[data-tool="select"]').first().click();
|
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
|
const midX = startX + 60;
|
|
|
|
|
const midY = startY + 30;
|
|
|
|
|
await page.mouse.click(midX, midY);
|
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
|
const sel = await page.evaluate(() => {
|
|
|
|
|
const w = window as unknown as { __v2GetSelection?: () => { ids: string[] } };
|
|
|
|
|
return w.__v2GetSelection?.() ?? null;
|
|
|
|
|
});
|
|
|
|
|
expect(sel, 'Selection-Bridge muss erreichbar sein').toBeTruthy();
|
|
|
|
|
expect(sel!.ids.length, `Genau 1 Element muss ausgewaehlt sein, got ${sel!.ids.length}`).toBe(1);
|
2026-09-16 23:25:22 +02:00
|
|
|
|
|
|
|
|
// ABNAHME 4: Tool-Optionen erreichen das Werkzeug (Phase 4a).
|
|
|
|
|
// Traversen-Werkzeug waehlen, chordCount=3 im Options-Panel setzen,
|
|
|
|
|
// Traverse ziehen, properties.chordCount muss 3 sein.
|
|
|
|
|
await page.locator('[data-v2tool="truss"], [data-tool="truss"]').first().click();
|
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
|
const chordInput = page.locator('.tool-options-panel input[type=number]').first();
|
|
|
|
|
// chordCount-Feld ueber Label finden (Gurtrohre)
|
|
|
|
|
const chordRow = page.locator('.toolopt-row', { hasText: 'Gurtrohre' });
|
|
|
|
|
await chordRow.locator('input').fill('3');
|
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
|
// Traverse ziehen (wie Linie)
|
|
|
|
|
const tStart = cx - 160;
|
|
|
|
|
const tStartY = cy + 120;
|
|
|
|
|
await page.mouse.move(tStart, tStartY);
|
|
|
|
|
await page.mouse.down();
|
|
|
|
|
await page.mouse.move(tStart + 150, tStartY, { steps: 6 });
|
|
|
|
|
await page.mouse.up();
|
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
|
const trussEl = await page.evaluate(() => {
|
|
|
|
|
const w = window as unknown as { __cad14Truss?: { id: string; chordCount: number } | null };
|
|
|
|
|
return w.__cad14Truss ?? null;
|
|
|
|
|
});
|
|
|
|
|
// Alternativ: ueber die DB pruefen (Autosave-Poll) — rohe DB-Zeile hat
|
|
|
|
|
// properties_json (String), nicht properties.
|
|
|
|
|
let chordInDb: unknown = null;
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
await page.waitForTimeout(5000);
|
|
|
|
|
const drs = await (await fetch(`${BASE_URL}/api/projects/${proj.id}/drawings`, { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
|
|
|
let found = false;
|
|
|
|
|
for (const d of drs) {
|
|
|
|
|
const raw = await (await fetch(`${BASE_URL}/api/drawings/${d.id}/elements`, { headers: { Authorization: `Bearer ${token}` } })).json();
|
|
|
|
|
for (const e of raw) {
|
|
|
|
|
const parsed = typeof e.properties_json === 'string' ? JSON.parse(e.properties_json) : (e.properties_json ?? {});
|
|
|
|
|
if (String(e.type) === 'truss' && (parsed as any)?.chordCount === 3) {
|
|
|
|
|
chordInDb = parsed.chordCount;
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (found) break;
|
|
|
|
|
}
|
|
|
|
|
if (chordInDb !== null) break;
|
|
|
|
|
}
|
|
|
|
|
expect(String(chordInDb), 'chordCount=3 muss beim gezeichneten Element ankommen').toBe('3');
|
2026-09-16 18:03:08 +02:00
|
|
|
});
|