task(A0): logger and test helpers
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Minimaler Level-basierter Logger.
|
||||||
|
* Ziel: direktes `console.log` im Produktivcode ersetzen (ROADMAP Teil-4-Regel),
|
||||||
|
* damit sich Logs zentral filtern/abschalten lassen.
|
||||||
|
*
|
||||||
|
* Nutzung:
|
||||||
|
* import { logger } from '../utils/logger';
|
||||||
|
* logger.info('Element erstellt', el.id);
|
||||||
|
*
|
||||||
|
* Level über localStorage['webcad.logLevel'] ('silent'|'error'|'warn'|'info'|'debug',
|
||||||
|
* Default: 'info'). `logger.debug` bleibt auf Default still.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
|
||||||
|
|
||||||
|
const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||||
|
silent: 100,
|
||||||
|
error: 40,
|
||||||
|
warn: 30,
|
||||||
|
info: 20,
|
||||||
|
debug: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
function readConfiguredLevel(): LogLevel {
|
||||||
|
try {
|
||||||
|
// Browser und jsdom haben localStorage; in reinen Node-Tests schlägt der Zugriff fehl
|
||||||
|
const raw = globalThis.localStorage?.getItem('webcad.logLevel');
|
||||||
|
if (raw && raw in LEVEL_ORDER) return raw as LogLevel;
|
||||||
|
} catch {
|
||||||
|
/* Umgebung ohne localStorage → Default */
|
||||||
|
}
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLevel: LogLevel = readConfiguredLevel();
|
||||||
|
|
||||||
|
/** Setzt das Level programmatisch (überschreibt localStorage bis zum Reload). */
|
||||||
|
export function setLogLevel(level: LogLevel): void {
|
||||||
|
currentLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aktuell aktives Log-Level. */
|
||||||
|
export function getLogLevel(): LogLevel {
|
||||||
|
return currentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
/** Fehlerhafte Zustände — melden außer bei silent. */
|
||||||
|
error(...args: unknown[]): void {
|
||||||
|
if (LEVEL_ORDER[currentLevel] <= LEVEL_ORDER.error) console.error('[web-cad]', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Warnungen. */
|
||||||
|
warn(...args: unknown[]): void {
|
||||||
|
if (LEVEL_ORDER[currentLevel] <= LEVEL_ORDER.warn) console.warn('[web-cad]', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normale Informationsmeldungen (Default-Level aktiv). */
|
||||||
|
info(...args: unknown[]): void {
|
||||||
|
if (LEVEL_ORDER[currentLevel] <= LEVEL_ORDER.info) console.log('[web-cad]', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debug-Ausgaben — nur wenn Level debug konfiguriert ist. */
|
||||||
|
debug(...args: unknown[]): void {
|
||||||
|
if (LEVEL_ORDER[currentLevel] <= LEVEL_ORDER.debug) console.log('[web-cad][debug]', ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user