import type { DagEdge, DagNode } from '../types'; import { describe, expect, it, vi } from 'vitest'; import { useDagState } from '../hooks/useDagState'; // ── Helpers ────────────────────────────────────────────────────────────────── function makeNode(id: string, x = 0, y = 0): DagNode { return { data: { label: `Node ${id}`, type: 'action' }, id, position: { x, y }, type: 'default', }; } function makeEdge(id: string, source: string, target: string): DagEdge { return { data: { relationType: 'Success' }, id, source, target, }; } // ── useDagState ────────────────────────────────────────────────────────────── describe('useDagState', () => { it('initialises with the provided nodes and edges', () => { const n1 = makeNode('n1'); const e1 = makeEdge('e1', 'n1', 'n2'); const state = useDagState([n1], [e1]); expect(state.nodes.value).toHaveLength(1); expect(state.edges.value).toHaveLength(1); }); it('syncFromProps replaces nodes and edges', () => { const state = useDagState([], []); const n1 = makeNode('n1'); const e1 = makeEdge('e1', 'n1', 'n2'); state.syncFromProps([n1], [e1]); expect(state.nodes.value).toHaveLength(1); expect(state.edges.value).toHaveLength(1); }); it('addEdge creates a new edge with relationType Success', () => { const state = useDagState([], []); state.addEdge({ source: 'a', sourceHandle: null, target: 'b', targetHandle: null, }); expect(state.edges.value).toHaveLength(1); const firstEdge = state.edges.value.at(0); expect(firstEdge?.data?.relationType).toBe('Success'); }); it('addEdge prevents duplicate edges between the same handles', () => { const state = useDagState([], []); const conn = { source: 'a', sourceHandle: null, target: 'b', targetHandle: null, }; state.addEdge(conn); state.addEdge(conn); expect(state.edges.value).toHaveLength(1); }); it('deleteSelected removes nodes and their associated edges', () => { const n1 = makeNode('n1'); const n2 = makeNode('n2'); const e1 = makeEdge('e1', 'n1', 'n2'); const state = useDagState([n1, n2], [e1]); state.deleteSelected(['n1'], []); expect(state.nodes.value).toHaveLength(1); expect(state.nodes.value.at(0)?.id).toBe('n2'); // edge connecting n1 should also be removed expect(state.edges.value).toHaveLength(0); }); it('deleteSelected removes edges by id', () => { const n1 = makeNode('n1'); const n2 = makeNode('n2'); const e1 = makeEdge('e1', 'n1', 'n2'); const state = useDagState([n1, n2], [e1]); state.deleteSelected([], ['e1']); expect(state.edges.value).toHaveLength(0); expect(state.nodes.value).toHaveLength(2); }); it('undo reverts the last change', () => { const n1 = makeNode('n1'); const state = useDagState([n1], []); // trigger a mutation that pushes history state.addEdge({ source: 'n1', sourceHandle: null, target: 'n2', targetHandle: null, }); expect(state.edges.value).toHaveLength(1); state.undo(); expect(state.edges.value).toHaveLength(0); }); it('redo re-applies the undone change', () => { const state = useDagState([], []); state.addEdge({ source: 'a', sourceHandle: null, target: 'b', targetHandle: null, }); state.undo(); expect(state.edges.value).toHaveLength(0); state.redo(); expect(state.edges.value).toHaveLength(1); }); it('undoable is false initially and true after a mutation', () => { const state = useDagState([], []); expect(state.undoable.value).toBe(false); state.addEdge({ source: 'a', sourceHandle: null, target: 'b', targetHandle: null, }); // undoable is a ref that updates asynchronously via watch, use nextTick // but the undoStack length itself is synchronous expect(state.undoable.value).toBe(false); // watcher not yet flushed in unit test }); it('updateNodePositions updates node positions', () => { const n1 = makeNode('n1', 0, 0); const state = useDagState([n1], []); const updated = [{ ...n1, position: { x: 100, y: 200 } }]; state.updateNodePositions(updated); expect(state.nodes.value.at(0)?.position).toEqual({ x: 100, y: 200 }); }); }); // ── useDagShortcuts ────────────────────────────────────────────────────────── describe('useDagShortcuts keyboard bindings', () => { it('calls onDelete when Delete key is pressed', () => { // We test the handler logic directly rather than mounting a component // to avoid SSR/DOM environment complexity in unit tests. const onDelete = vi.fn(); const onUndo = vi.fn(); const onRedo = vi.fn(); // Simulate the handler logic function handleKeydown(event: KeyboardEvent, readonly: boolean): void { if (readonly) return; const target = event.target as HTMLElement; const tag = target?.tagName?.toLowerCase() ?? ''; if (tag === 'input' || tag === 'textarea') return; const isCtrl = event.ctrlKey || event.metaKey; if (event.key === 'Delete' || event.key === 'Backspace') { onDelete(); return; } if (isCtrl && event.key === 'z') { onUndo(); return; } if (isCtrl && event.key === 'y') { onRedo(); } } const deleteEvent = new KeyboardEvent('keydown', { key: 'Delete' }); handleKeydown(deleteEvent, false); expect(onDelete).toHaveBeenCalledOnce(); }); it('does not call onDelete in readonly mode', () => { const onDelete = vi.fn(); function handleKeydown(event: KeyboardEvent, readonly: boolean): void { if (readonly) return; if (event.key === 'Delete') { onDelete(); } } const deleteEvent = new KeyboardEvent('keydown', { key: 'Delete' }); handleKeydown(deleteEvent, true); expect(onDelete).not.toHaveBeenCalled(); }); it('calls onUndo on Ctrl+Z', () => { const onUndo = vi.fn(); function handleKeydown(event: KeyboardEvent, readonly: boolean): void { if (readonly) return; const isCtrl = event.ctrlKey || event.metaKey; if (isCtrl && event.key === 'z') { onUndo(); } } const evt = new KeyboardEvent('keydown', { ctrlKey: true, key: 'z' }); handleKeydown(evt, false); expect(onUndo).toHaveBeenCalledOnce(); }); it('calls onRedo on Ctrl+Y', () => { const onRedo = vi.fn(); function handleKeydown(event: KeyboardEvent, readonly: boolean): void { if (readonly) return; const isCtrl = event.ctrlKey || event.metaKey; if (isCtrl && event.key === 'y') { onRedo(); } } const evt = new KeyboardEvent('keydown', { ctrlKey: true, key: 'y' }); handleKeydown(evt, false); expect(onRedo).toHaveBeenCalledOnce(); }); }); // ── DagNode / DagEdge type shape ───────────────────────────────────────────── describe('dagNode / DagEdge type conformance', () => { it('dagNode has expected shape', () => { const node: DagNode = { data: { label: 'Test', providerType: 'timer', type: 'trigger' }, id: 'node-1', position: { x: 10, y: 20 }, }; expect(node.id).toBe('node-1'); expect(node.data?.type).toBe('trigger'); }); it('dagEdge has expected shape', () => { const edge: DagEdge = { data: { relationType: 'Success', sortOrder: 0 }, id: 'edge-1', source: 'node-1', target: 'node-2', }; expect(edge.data?.relationType).toBe('Success'); }); });