task(A0): logger and test helpers

This commit is contained in:
Agent Zero
2026-08-26 21:04:17 +02:00
parent 725879562e
commit cba9358235
4 changed files with 264 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
/**
* Smoke-Tests für die Test-Helfer aus Task A0.
* Stellt sicher, dass Logger, MockRenderEngine und InMemoryDoc funktionieren,
* BEVOR spätere Tasks auf ihnen aufbauen.
*/
import { describe, it, expect, vi } from 'vitest';
import { logger, setLogLevel } from '../src/utils/logger';
import { MockRenderEngine } from './helpers/mockAdapter';
import { createInMemoryDoc, makeTestLayer } from './helpers/inMemoryDoc';
import type { CADElement } from '../src/types/cad.types';
describe('A0 helpers logger', () => {
it('schreibt info im Default-Level in die Testumgebung', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
setLogLevel('info');
logger.info('smoke test message');
expect(spy).toHaveBeenCalledWith('[web-cad]', 'smoke test message');
spy.mockRestore();
});
it('debug bleibt bei Level info still und erscheint bei debug', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
setLogLevel('info');
logger.debug('hidden');
expect(spy).not.toHaveBeenCalled();
setLogLevel('debug');
logger.debug('visible');
expect(spy).toHaveBeenCalledWith('[web-cad][debug]', 'visible');
spy.mockRestore();
});
});
describe('A0 helpers MockRenderEngine', () => {
it('protokolliert Aufrufe mit Reihenfolge und Argumenten', () => {
const mock = new MockRenderEngine();
mock.render();
mock.hitTest(10, 20, 5);
mock.setPreviewElement(null);
expect(mock.countCalls('render')).toBe(1);
expect(mock.countCalls('hitTest')).toBe(1);
expect(mock.lastArgsOf('hitTest')).toEqual([10, 20, 5]);
expect(mock.calls.map((c) => c.method)).toEqual(['render', 'hitTest', 'setPreviewElement']);
});
it('liefert konfigurierbare hitTest-Ergebnisse', () => {
const mock = new MockRenderEngine();
const el = { id: 'e1', type: 'line' } as unknown as CADElement;
mock.nextHitTestResult = el;
expect(mock.hitTest(0, 0)).toBe(el);
mock.reset();
expect(mock.hitTest(0, 0)).toBeNull();
});
});
describe('A0 helpers createInMemoryDoc', () => {
it('erstellt netzwerkfreies Dokument mit Seeding', () => {
const { ydoc, addAll } = createInMemoryDoc();
ydoc.data.layers.set('layer-1', makeTestLayer() as never);
const el = { id: 'el1', type: 'rect', layerId: 'layer-1', x: 0, y: 0, width: 1, height: 1, properties: {} } as unknown as CADElement;
addAll([el]);
expect(ydoc.getElements().length).toBe(1);
expect(ydoc.getLayers().length).toBe(1);
});
it('clear entfernt alle Elemente', () => {
const { ydoc, addAll, clear } = createInMemoryDoc();
const el = { id: 'el1', type: 'rect', layerId: 'layer-1', x: 0, y: 0, width: 1, height: 1, properties: {} } as unknown as CADElement;
addAll([el]);
expect(ydoc.getElements().length).toBe(1);
clear();
expect(ydoc.getElements().length).toBe(0);
});
});
+52
View File
@@ -0,0 +1,52 @@
/**
* InMemoryDoc-Helper Netzwerkfreie YjsDocument-Fabrik für Tests.
*
* YjsDocument läuft bereits komplett In-Memory (Y.Doc ohne Provider);
* dieser Wrapper bietet bequemes Seeding und Reset für isolierte Tests.
* Genutzt ab A3 (Tool-Workflow-Tests) und A2b (CADDocument-Tests).
*/
import { YjsDocument } from '../../src/crdt/YjsDocument';
import type { CADElement } from '../../src/types/cad.types';
export interface SeededDoc {
ydoc: YjsDocument;
/** Fügt Elemente in EINER Transaktion hinzu (1 Undo-Schritt). */
addAll(elements: CADElement[]): void;
/** Entfernt alle Elemente/Layer-Inhalte (Dokument bleibt verwendbar). */
clear(): void;
}
/** Erzeugt ein frisches, leeres Test-Dokument. */
export function createInMemoryDoc(): SeededDoc {
const ydoc = new YjsDocument();
return {
ydoc,
addAll(elements) {
ydoc.doc.transact(() => {
for (const el of elements) ydoc.data.elements.set(el.id, el);
});
},
clear() {
ydoc.doc.transact(() => {
for (const id of Array.from(ydoc.data.elements.keys())) {
ydoc.data.elements.delete(id);
}
});
},
};
}
/** Basis-CADLayer für Tests. */
export function makeTestLayer(id = 'layer-1', name = 'Test-Layer') {
return {
id,
name,
visible: true,
locked: false,
color: '#ffffff',
lineType: 'solid',
transparency: 0,
sortOrder: 0,
parentId: null,
};
}
+69
View File
@@ -0,0 +1,69 @@
/**
* MockAdapter Headless-Doppel der RenderEngine für Workflow-Tests.
* Zeichnet nichts; protokolliert alle Aufrufe in `calls` (Reihenfolge getreu).
*
* Methodenumfang = genau das, was InteractionEngine derzeit an RenderEngine
* ruft (verifiziert in Task A0): hitTest, render, setActiveSnapPoint,
* setOptions, setPreviewElement, setSnapPoints.
* Wird in C1 (RenderAdapter-Interface) um die vollen Adapter-Methoden erweitert.
*/
import type { CADElement } from '../../src/types/cad.types';
export interface RecordedCall {
method: string;
args: unknown[];
}
export class MockRenderEngine {
/** Alle Aufrufe in Reihenfolge. */
readonly calls: RecordedCall[] = [];
/** Von Tests konfigurierbare hitTest-Rückgabe. */
nextHitTestResult: CADElement | null = null;
// ── Von InteractionEngine genutzte Fläche ────────────────
hitTest(x: number, y: number, tolerance?: number): CADElement | null {
this.calls.push({ method: 'hitTest', args: [x, y, tolerance] });
return this.nextHitTestResult;
}
render(): void {
this.calls.push({ method: 'render', args: [] });
}
setActiveSnapPoint(pt: unknown): void {
this.calls.push({ method: 'setActiveSnapPoint', args: [pt] });
}
setOptions(opts: Record<string, unknown>): void {
this.calls.push({ method: 'setOptions', args: [opts] });
}
setPreviewElement(el: CADElement | null): void {
this.calls.push({ method: 'setPreviewElement', args: [el] });
}
setSnapPoints(pts: unknown[]): void {
this.calls.push({ method: 'setSnapPoints', args: [pts] });
}
// ── Test-Helfer ──────────────────────────────────────────
/** Anzahl Aufrufe einer Methode. */
countCalls(method: string): number {
return this.calls.filter((c) => c.method === method).length;
}
/** Letzte aufgezeichnete Argumentliste einer Methode (oder undefined). */
lastArgsOf(method: string): unknown[] | undefined {
const found = this.calls.filter((c) => c.method === method);
return found.length ? found[found.length - 1].args : undefined;
}
/** Protokoll leeren. */
reset(): void {
this.calls.length = 0;
this.nextHitTestResult = null;
}
}