174 lines
6.4 KiB
TypeScript
174 lines
6.4 KiB
TypeScript
/**
|
||
* 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
|
||
});
|
||
});
|