/** * Regressions-Tests für bekannte Bugs BUG-1, BUG-1b, BUG-2. * Diese Tests sind VOR dem jeweiligen Fix ROT (dokumentiertes Fehlverhalten) * und müssen danach GRÜN sein. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { SeatingService } from '../src/services/seatingService'; import { trimElement, offsetElement } from '../src/tools/modification/geometry'; import type { CADElement } from '../src/types/cad.types'; import { RenderEngine } from '../src/canvas/RenderEngine'; import { ZoomPanController } from '../src/canvas/ZoomPanController'; import { SpatialIndex } from '../src/canvas/SpatialIndex'; import { LayerManager } from '../src/canvas/LayerManager'; // ── Helfer (Muster aus tests/geometry.test.ts) ───────────── function makeLine(id: string, x1: number, y1: number, x2: number, y2: number): CADElement { return { id, type: 'line', layerId: 'layer-1', x: (x1 + x2) / 2, y: (y1 + y2) / 2, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), properties: { x1, y1, x2, y2 }, } as unknown as CADElement; } function makeCircle(id: string, cx: number, cy: number, r: number): CADElement { return { id, type: 'circle', layerId: 'layer-1', x: cx, y: cy, width: r * 2, height: r * 2, properties: { radius: r }, } as unknown as CADElement; } // ── BUG-1: rowId-Kollision bei schnellem Erzeugen ─────────── describe('BUG-1: seating row ids', () => { it('zwei direkt hintereinander erzeugte Reihen haben unterschiedliche rowIds', () => { const svc = new SeatingService(); const a = svc.createSeatingRow(0, 0, 'layer-1'); const b = svc.createSeatingRow(0, 100, 'layer-1'); const ids = new Set([a[0].properties.rowId, b[0].properties.rowId]); expect(ids.size).toBe(2); }); }); // ── BUG-1b: Block-Reihen ohne eigene rowId ────────────────── describe('BUG-1b: seating block row ids', () => { it('jede Reihe eines Blocks hat eine eigene, definierte rowId', () => { const svc = new SeatingService(); const els = svc.createSeatingBlock(0, 0, 'layer-1', { rows: 3, cols: 2 }); expect(els.length).toBe(6); const rowIds = new Set(els.map((e) => e.properties.rowId)); expect(rowIds.size).toBe(3); for (const rid of rowIds) expect(rid).toBeDefined(); }); }); // ── BUG-2: Trim an Kreis scheitert (findIntersection nur Gerade×Gerade) ── describe('BUG-2: trim line at circle', () => { it('trimElement(Linie, Kreis) liefert ein Ergebnis statt null', () => { // Linie von (0,0) nach (200,0) schneidet Kreis (Zentrum 100,0, Radius 40) // an x=60 und x=140 → Schnitt existiert eindeutig. const line = makeLine('l1', 0, 0, 200, 0); const circle = makeCircle('c1', 100, 0, 40); const result = trimElement(line, circle); expect(result).not.toBeNull(); }); }); // ── BUG-3: Offset ignorierte die Klickseite ───────────────── describe('BUG-3: offset respects click side', () => { it('side=-1 versetzt Linie entgegen der Normalenrichtung', () => { // Linie (100,100)→(300,100): Laufrichtung +x. // Normale (-dy,dx)=(0,1) zeigt in Canvas-Koordinaten nach UNTEN (+y). const line = makeLine('l1', 100, 100, 300, 100); const result = offsetElement(line, 20, -1); expect(result.properties.y1).toBeCloseTo(80); // nach OBEN versetzt expect(result.properties.y2).toBeCloseTo(80); }); it('side=+1 versetzt Linie in Normalenrichtung (Default wie vorher)', () => { const line = makeLine('l1', 100, 100, 300, 100); const result = offsetElement(line, 20, 1); expect(result.properties.y1).toBeCloseTo(120); // nach UNTEN versetzt expect(result.properties.y2).toBeCloseTo(120); }); it('ohne side bleibt Verhalten identisch zur alten API (Rueckwaertskompatibilitaet)', () => { const line = makeLine('l1', 100, 100, 300, 100); const result = offsetElement(line, 20); expect(result.properties.y1).toBeCloseTo(120); }); it('Kreis: side=-1 verkleinert Radius, side=+1 vergroessert', () => { const circle = makeCircle('c1', 0, 0, 50); expect(offsetElement(circle, 10, -1).properties.radius).toBeCloseTo(40); expect(offsetElement(circle, 10, 1).properties.radius).toBeCloseTo(60); }); }); // ── BUG-4: Hintergrundbild wurde pro Frame neu geladen ────── describe('BUG-4: background image cached', () => { let imageCount: number; let OriginalImage: typeof Image; beforeEach(() => { imageCount = 0; OriginalImage = global.Image; // Bild-Stub: zaehlt Konstruktur-Aufrufe; simuliert Browser-Cache-Bild // (complete=true sofort verfuegbar, onload als Mikrotask wie async decode) class FakeImage { complete = true; naturalWidth = 800; width = 800; height = 600; onload: (() => void) | null = null; private internalSrc = ''; get src(): string { return this.internalSrc; } set src(value: string) { this.internalSrc = value; queueMicrotask(() => this.onload?.()); } constructor() { imageCount += 1; } } (global as { Image: unknown }).Image = FakeImage as unknown as typeof Image; }); afterEach(() => { (global as { Image: unknown }).Image = OriginalImage; }); function createEngine(): RenderEngine { const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; const zoomPan = new ZoomPanController(canvas); const spatialIndex = new SpatialIndex(); const layerManager = new LayerManager(); return new RenderEngine(canvas, zoomPan, spatialIndex, layerManager); } it('lädt das Bild pro Quellwechsel nur EINMAL, weitere Frames nutzen den Cache', async () => { const engine = createEngine(); const DATA_URL = 'data:image/png;base64,iVBORw0KGgo='; engine.setOptions({ backgroundSrc: DATA_URL, backgroundOpacity: 0.5 }); // Frame 1: legt genau ein Image an und cachet es (complete=true) engine.render(); expect(imageCount).toBe(1); // Weitere Frames mit identischer src: dürfen KEIN neues Image erzeugen engine.render(); engine.render(); engine.render(); expect(imageCount).toBe(1); // Ausstehende Mikrotasks (simuliertes onload) abwarten, dann erneut prüfen: await Promise.resolve(); engine.render(); expect(imageCount).toBe(1); // vorher (Bug): jedes render() erzeugte ein neues Image }); });