/** * Pilot V2-Tools Tests (Task A3): line + select. * line: Handler-Sequenz gegen echtes CADDocument (In-Memory), Undo-Verhalten. * select: Bridge-basierte Auswahl mit Shift/Ctrl. */ import { describe, it, expect, vi } from 'vitest'; import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing'; import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import type { ToolPointerEvent, FullToolContext } from '../src/plugins/types'; import type { CADElement } from '../src/types/cad.types'; function pe(x: number, y: number, mods: Partial> = {}): ToolPointerEvent { return { world: { x, y }, screen: { x: x / 2, y: y / 2 }, button: 0, shift: mods.shift ?? false, alt: false, ctrl: mods.ctrl ?? false, }; } function makeCtx(overrides: Partial = {}): FullToolContext & { previews: (CADElement | null)[]; statuses: string[] } { const previews: (CADElement | null)[] = []; const statuses: string[] = []; return { options: {}, setStatus: (m) => statuses.push(m), setPreview: (el) => previews.push(el), previews, statuses, ...overrides, } as FullToolContext & { previews: (CADElement | null)[]; statuses: string[] }; } function makeEl(id: string): CADElement { return { id, type: 'rect', layerId: 'l', x: 0, y: 0, width: 1, height: 1, properties: {} } as unknown as CADElement; } describe('A3 pilot: line tool', () => { it('erzeugt EINE Linie per down→up; ein undo entfernt sie', () => { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); const ctx = makeCtx({ doc }); lineTool.handlers.down!(pe(0, 0), ctx); lineTool.handlers.move!(pe(100, 50), ctx); expect(ctx.previews.length).toBe(1); // Live-Vorschau war da lineTool.handlers.up!(pe(100, 50), ctx); const all = doc.getAllElements(); expect(all.length).toBe(1); expect(all[0].type).toBe('line'); expect(all[0].properties.x1).toBe(0); expect(all[0].properties.y2).toBe(50); // Kern-Akzeptanz aus ROADMAP: Ein Werkzeug-Klick = ein Undo-Schritt expect(doc.undo()).toBe(true); expect(doc.getAllElements().length).toBe(0); }); it('cancel verwirft die laufende Linie ohne Element', () => { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); const ctx = makeCtx({ doc }); lineTool.handlers.down!(pe(5, 5), ctx); lineTool.handlers.cancel!(ctx); lineTool.handlers.up!(pe(9, 9), ctx); // up nach cancel: kein Start mehr expect(doc.getAllElements().length).toBe(0); }); it('Plugin-Manifest ist registrierbar und tagged basic', () => { expect(coreDrawingPlugin.manifest.enabledByDefault).toBe(true); const line = coreDrawingPlugin.tools?.find((t) => t.manifest.id === 'line'); expect(line?.manifest.tags).toContain('basic'); }); }); describe('A3 pilot: select tool', () => { function makeBridge(elements: CADElement[]) { let ids: string[] = []; return { bridge: { getIds: () => ids, setIds: (next: string[]) => { ids = next; }, hitTest: (wx: number, wy: number, tol: number): CADElement | null => elements.find((el) => Math.abs(el.x - wx) <= tol && Math.abs(el.y - wy) <= tol) ?? null, }, getIds: () => ids, }; } it('wählt getroffenes Element ohne Modifier exklusiv', () => { const els = [makeEl('a'), makeEl('b')]; els[0].x = 10; els[0].y = 10; els[1].x = 500; els[1].y = 500; const { bridge, getIds } = makeBridge(els); const ctx = makeCtx({ selection: bridge }); selectTool.handlers.down!(pe(10, 10), ctx); expect(getIds()).toEqual(['a']); // Klick auf b ohne Shift ersetzt Selektion selectTool.handlers.down!(pe(500, 500), ctx); expect(getIds()).toEqual(['b']); }); it('Shift addiert, Ctrl subtrahiert', () => { const els = [makeEl('a'), makeEl('b'), makeEl('c')]; els[0].x = 10; els[0].y = 10; els[1].x = 50; els[1].y = 50; els[2].x = 90; els[2].y = 90; const { bridge, getIds } = makeBridge(els); const ctx = makeCtx({ selection: bridge }); selectTool.handlers.down!(pe(10, 10), ctx); expect(getIds()).toEqual(['a']); selectTool.handlers.down!(pe(50, 50, { shift: true }), ctx); expect(getIds().sort()).toEqual(['a', 'b']); selectTool.handlers.down!(pe(90, 90, { shift: true }), ctx); expect(getIds().sort()).toEqual(['a', 'b', 'c']); selectTool.handlers.down!(pe(50, 50, { ctrl: true }), ctx); expect(getIds().sort()).toEqual(['a', 'c']); }); it('Leerklick ohne Modifier leert die Selektion; Plugin tagged basic', () => { const els = [makeEl('a')]; els[0].x = 10; els[0].y = 10; const { bridge, getIds } = makeBridge(els); const ctx = makeCtx({ selection: bridge }); selectTool.handlers.down!(pe(10, 10), ctx); expect(getIds()).toEqual(['a']); selectTool.handlers.down!(pe(999, 999), ctx); expect(getIds()).toEqual([]); expect(coreModifyPlugin.manifest.enabledByDefault).toBe(true); const sel = coreModifyPlugin.tools?.find((t) => t.manifest.id === 'select'); expect(sel?.manifest.tags).toContain('basic'); }); });