import { describe, it, expect } from 'vitest'; import { ComponentGraphEngine, computeHealthFromData } from './engine.js'; import { serializeGraph, deserializeGraph } from './serialization.js'; import type { ComponentGraph, ComponentNode, GraphEdge, GraphEdgeType, } from './types.js'; import { GRAPH_EDGE_TYPES, EDGE_TYPE_WEIGHTS } from './types.js'; // --------------------------------------------------------------------------- // Test fixtures — 6-node graph // --------------------------------------------------------------------------- function createTestNodes(): ComponentNode[] { return [ { name: 'Button', category: 'actions', status: 'stable', compositionPattern: 'simple' }, { name: 'Dialog', category: 'overlays', status: 'stable', compositionPattern: 'compound', subComponents: ['Trigger', 'Content', 'Header', 'Body', 'Footer', 'Close'] }, { name: 'Header', category: 'navigation', status: 'stable', compositionPattern: 'compound', subComponents: ['Brand', 'Nav', 'Actions'] }, { name: 'Sidebar', category: 'navigation', status: 'stable', compositionPattern: 'compound', subComponents: ['Toggle', 'Content', 'Footer'] }, { name: 'AppShell', category: 'layout', status: 'stable', compositionPattern: 'compound', subComponents: ['Header', 'Sidebar', 'Main'] }, { name: 'Input', category: 'forms', status: 'stable', compositionPattern: 'simple' }, ]; } function createTestEdges(): GraphEdge[] { return [ // AppShell imports Header and Sidebar { source: 'AppShell', target: 'Header', type: 'imports', weight: 1.0, provenance: 'source:AppShell/index.tsx' }, { source: 'AppShell', target: 'Sidebar', type: 'imports', weight: 1.0, provenance: 'source:AppShell/index.tsx' }, // Header imports Button { source: 'Header', target: 'Button', type: 'imports', weight: 1.0, provenance: 'source:Header/index.tsx' }, // Dialog renders Button in variants { source: 'Dialog', target: 'Button', type: 'renders', weight: 0.5, provenance: 'variant:Default' }, // Dialog and Sidebar use hooks from each other (fictional) { source: 'Dialog', target: 'Sidebar', type: 'hook-depends', weight: 0.75, provenance: 'source:Dialog/index.tsx' }, // AppShell and Dialog compose in a block { source: 'AppShell', target: 'Dialog', type: 'composes', weight: 0.5, provenance: 'block:DashboardLayout' }, // Header parent-of Button (Button is a sub-component in Header) { source: 'Header', target: 'Button', type: 'parent-of', weight: 1.0, provenance: 'relation' }, // Dialog alternative to a Popover (using Header as proxy) { source: 'Dialog', target: 'Header', type: 'alternative-to', weight: 1.0, provenance: 'relation', note: 'Dialog provides modal overlay, Header provides persistent navigation' }, // Header sibling of Sidebar { source: 'Header', target: 'Sidebar', type: 'sibling-of', weight: 0.75, provenance: 'relation' }, ]; } function createTestBlocks(): Record { return { 'DashboardLayout': { components: ['AppShell', 'Header', 'Sidebar'] }, 'LoginForm': { components: ['Dialog', 'Input', 'Button'] }, 'SearchBar': { components: ['Input', 'Button'] }, }; } function createTestGraph(): ComponentGraph { const nodes = createTestNodes(); const edges = createTestEdges(); const blockIndex = new Map(); const blocks = createTestBlocks(); for (const [blockName, block] of Object.entries(blocks)) { for (const comp of block.components) { const existing = blockIndex.get(comp); if (existing) existing.push(blockName); else blockIndex.set(comp, [blockName]); } } const health = computeHealthFromData(nodes, edges, blockIndex); return { nodes, edges, health }; } function createEngine(): ComponentGraphEngine { return new ComponentGraphEngine(createTestGraph(), createTestBlocks()); } // --------------------------------------------------------------------------- // Type constants tests // --------------------------------------------------------------------------- describe('graph types', () => { it('defines all 7 edge types', () => { expect(GRAPH_EDGE_TYPES).toHaveLength(7); expect(GRAPH_EDGE_TYPES).toContain('imports'); expect(GRAPH_EDGE_TYPES).toContain('hook-depends'); expect(GRAPH_EDGE_TYPES).toContain('renders'); expect(GRAPH_EDGE_TYPES).toContain('composes'); expect(GRAPH_EDGE_TYPES).toContain('parent-of'); expect(GRAPH_EDGE_TYPES).toContain('alternative-to'); expect(GRAPH_EDGE_TYPES).toContain('sibling-of'); }); it('assigns correct default weights', () => { expect(EDGE_TYPE_WEIGHTS['imports']).toBe(1.0); expect(EDGE_TYPE_WEIGHTS['hook-depends']).toBe(0.75); expect(EDGE_TYPE_WEIGHTS['renders']).toBe(0.5); expect(EDGE_TYPE_WEIGHTS['composes']).toBe(0.5); expect(EDGE_TYPE_WEIGHTS['parent-of']).toBe(1.0); expect(EDGE_TYPE_WEIGHTS['alternative-to']).toBe(1.0); expect(EDGE_TYPE_WEIGHTS['sibling-of']).toBe(0.75); }); }); // --------------------------------------------------------------------------- // Engine — core queries // --------------------------------------------------------------------------- describe('ComponentGraphEngine', () => { describe('constructor', () => { it('builds adjacency lists from graph data', () => { const engine = createEngine(); expect(engine.hasNode('Button')).toBe(true); expect(engine.hasNode('NonExistent')).toBe(false); }); it('stores node metadata', () => { const engine = createEngine(); const node = engine.getNode('Dialog'); expect(node).toBeDefined(); expect(node!.category).toBe('overlays'); expect(node!.compositionPattern).toBe('compound'); expect(node!.subComponents).toContain('Trigger'); }); }); describe('dependencies()', () => { it('returns outgoing edges', () => { const engine = createEngine(); const deps = engine.dependencies('AppShell'); expect(deps.length).toBeGreaterThanOrEqual(2); expect(deps.some(e => e.target === 'Header' && e.type === 'imports')).toBe(true); expect(deps.some(e => e.target === 'Sidebar' && e.type === 'imports')).toBe(true); }); it('filters by edge type', () => { const engine = createEngine(); const importDeps = engine.dependencies('AppShell', ['imports']); expect(importDeps.every(e => e.type === 'imports')).toBe(true); expect(importDeps.length).toBe(2); // Header, Sidebar }); it('returns empty array for unknown component', () => { const engine = createEngine(); expect(engine.dependencies('NonExistent')).toEqual([]); }); it('returns empty array for component with no outgoing edges', () => { const engine = createEngine(); // Input has no outgoing edges in our test graph expect(engine.dependencies('Input')).toEqual([]); }); }); describe('dependents()', () => { it('returns incoming edges', () => { const engine = createEngine(); const deps = engine.dependents('Button'); expect(deps.length).toBeGreaterThanOrEqual(2); expect(deps.some(e => e.source === 'Header')).toBe(true); expect(deps.some(e => e.source === 'Dialog')).toBe(true); }); it('filters by edge type', () => { const engine = createEngine(); const renderDeps = engine.dependents('Button', ['renders']); expect(renderDeps.every(e => e.type === 'renders')).toBe(true); expect(renderDeps.length).toBe(1); // Dialog }); it('returns empty array for component with no dependents', () => { const engine = createEngine(); expect(engine.dependents('AppShell')).toEqual([]); }); }); describe('impact()', () => { it('finds transitively affected components', () => { const engine = createEngine(); const result = engine.impact('Button'); expect(result.component).toBe('Button'); // Header and Dialog depend on Button expect(result.affected.some(a => a.component === 'Header')).toBe(true); expect(result.affected.some(a => a.component === 'Dialog')).toBe(true); }); it('tracks depth levels', () => { const engine = createEngine(); const result = engine.impact('Button'); const headerEntry = result.affected.find(a => a.component === 'Header'); expect(headerEntry?.depth).toBe(1); // AppShell depends on Header, so it's at depth 2 const appShellEntry = result.affected.find(a => a.component === 'AppShell'); expect(appShellEntry?.depth).toBe(2); }); it('includes path from source', () => { const engine = createEngine(); const result = engine.impact('Button'); const headerEntry = result.affected.find(a => a.component === 'Header'); expect(headerEntry?.path).toEqual(['Button', 'Header']); }); it('respects maxDepth', () => { const engine = createEngine(); const result = engine.impact('Button', 1); // Only depth 1 — direct dependents expect(result.affected.every(a => a.depth === 1)).toBe(true); expect(result.affected.some(a => a.component === 'AppShell')).toBe(false); }); it('finds affected blocks', () => { const engine = createEngine(); const result = engine.impact('Button'); expect(result.affectedBlocks).toContain('DashboardLayout'); expect(result.affectedBlocks).toContain('LoginForm'); expect(result.affectedBlocks).toContain('SearchBar'); }); it('returns totalAffected count', () => { const engine = createEngine(); const result = engine.impact('Button'); expect(result.totalAffected).toBe(result.affected.length); }); it('handles component with no dependents', () => { const engine = createEngine(); const result = engine.impact('AppShell'); expect(result.totalAffected).toBe(0); expect(result.affected).toEqual([]); }); }); describe('path()', () => { it('finds shortest path between components', () => { const engine = createEngine(); const result = engine.path('Button', 'Sidebar'); expect(result.found).toBe(true); expect(result.path.length).toBeGreaterThanOrEqual(2); expect(result.path[0]).toBe('Button'); expect(result.path[result.path.length - 1]).toBe('Sidebar'); }); it('returns edges along the path', () => { const engine = createEngine(); const result = engine.path('Button', 'Sidebar'); expect(result.edges.length).toBe(result.path.length - 1); }); it('returns self-path for same component', () => { const engine = createEngine(); const result = engine.path('Button', 'Button'); expect(result.found).toBe(true); expect(result.path).toEqual(['Button']); expect(result.edges).toEqual([]); }); it('returns not found for disconnected components', () => { // Create a graph with an isolated node const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, ]; const graph: ComponentGraph = { nodes, edges: [], health: computeHealthFromData(nodes, []), }; const engine = new ComponentGraphEngine(graph); const result = engine.path('A', 'B'); expect(result.found).toBe(false); expect(result.path).toEqual([]); }); }); describe('islands()', () => { it('finds connected components', () => { const engine = createEngine(); const result = engine.islands(); expect(result.length).toBeGreaterThanOrEqual(1); // All 6 nodes should be connected in our test graph const allNodes = result.flat(); expect(allNodes).toContain('Button'); expect(allNodes).toContain('Dialog'); expect(allNodes).toContain('Header'); }); it('sorts largest island first', () => { const engine = createEngine(); const result = engine.islands(); for (let i = 1; i < result.length; i++) { expect(result[i - 1].length).toBeGreaterThanOrEqual(result[i].length); } }); it('detects isolated nodes as separate islands', () => { const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, { name: 'C', category: 'test', status: 'stable' }, ]; const edges: GraphEdge[] = [ { source: 'A', target: 'B', type: 'imports', weight: 1.0, provenance: 'test' }, ]; const graph: ComponentGraph = { nodes, edges, health: computeHealthFromData(nodes, edges), }; const engine = new ComponentGraphEngine(graph); const result = engine.islands(); expect(result.length).toBe(2); expect(result[0]).toEqual(['A', 'B']); expect(result[1]).toEqual(['C']); }); }); describe('neighbors()', () => { it('returns direct neighbors at 1 hop', () => { const engine = createEngine(); const result = engine.neighbors('Header'); expect(result.component).toBe('Header'); expect(result.neighbors.some(n => n.component === 'Button')).toBe(true); expect(result.neighbors.some(n => n.component === 'AppShell')).toBe(true); expect(result.neighbors.every(n => n.hops === 1)).toBe(true); }); it('expands to multi-hop neighbors', () => { const engine = createEngine(); const result = engine.neighbors('Header', 2); // At 2 hops we should also reach Dialog (via Button or Sidebar) expect(result.neighbors.some(n => n.component === 'Dialog')).toBe(true); }); it('returns empty for unknown component', () => { const engine = createEngine(); const result = engine.neighbors('NonExistent'); expect(result.neighbors).toEqual([]); }); }); // ------------------------------------------------------------------------- // Design-system queries // ------------------------------------------------------------------------- describe('composition()', () => { it('returns compound component tree', () => { const engine = createEngine(); const result = engine.composition('Dialog'); expect(result.component).toBe('Dialog'); expect(result.compositionPattern).toBe('compound'); expect(result.subComponents).toContain('Trigger'); expect(result.subComponents).toContain('Content'); }); it('finds parent from parent-of edges', () => { const engine = createEngine(); const result = engine.composition('Button'); // Header has a parent-of edge to Button expect(result.parent).toBe('Header'); }); it('finds siblings from sibling-of edges', () => { const engine = createEngine(); const result = engine.composition('Header'); expect(result.siblings).toContain('Sidebar'); }); it('finds blocks using the component', () => { const engine = createEngine(); const result = engine.composition('Button'); expect(result.blocks).toContain('LoginForm'); expect(result.blocks).toContain('SearchBar'); }); it('returns empty arrays for simple component', () => { const engine = createEngine(); const result = engine.composition('Input'); expect(result.subComponents).toEqual([]); expect(result.children).toEqual([]); expect(result.siblings).toEqual([]); }); }); describe('alternatives()', () => { it('finds alternative components', () => { const engine = createEngine(); const result = engine.alternatives('Dialog'); expect(result.some(a => a.component === 'Header')).toBe(true); }); it('includes notes on alternatives', () => { const engine = createEngine(); const result = engine.alternatives('Dialog'); const headerAlt = result.find(a => a.component === 'Header'); expect(headerAlt?.note).toBeDefined(); }); it('finds bidirectional alternatives', () => { const engine = createEngine(); // Header should also see Dialog as alternative (incoming edge) const result = engine.alternatives('Header'); expect(result.some(a => a.component === 'Dialog')).toBe(true); }); it('returns empty for component with no alternatives', () => { const engine = createEngine(); expect(engine.alternatives('Input')).toEqual([]); }); }); describe('blocksUsing()', () => { it('returns blocks that use a component', () => { const engine = createEngine(); expect(engine.blocksUsing('Button')).toContain('LoginForm'); expect(engine.blocksUsing('Button')).toContain('SearchBar'); }); it('returns empty for component not in any block', () => { // In our test blocks, all 6 components are used // Create a fresh graph with no blocks const engine = new ComponentGraphEngine(createTestGraph()); expect(engine.blocksUsing('Button')).toEqual([]); }); }); describe('subgraph()', () => { it('extracts induced subgraph', () => { const engine = createEngine(); const sub = engine.subgraph(['Button', 'Header', 'AppShell']); expect(sub.nodes.length).toBe(3); // Only edges between these 3 components for (const edge of sub.edges) { expect(['Button', 'Header', 'AppShell']).toContain(edge.source); expect(['Button', 'Header', 'AppShell']).toContain(edge.target); } }); it('recomputes health for subgraph', () => { const engine = createEngine(); const sub = engine.subgraph(['Button', 'Header']); expect(sub.health.nodeCount).toBe(2); }); it('handles empty component list', () => { const engine = createEngine(); const sub = engine.subgraph([]); expect(sub.nodes).toEqual([]); expect(sub.edges).toEqual([]); }); }); describe('getHealth()', () => { it('returns health metrics', () => { const engine = createEngine(); const health = engine.getHealth(); expect(health.nodeCount).toBe(6); expect(health.edgeCount).toBe(9); }); it('identifies hubs', () => { const engine = createEngine(); const health = engine.getHealth(); // Button, Header, Sidebar should be high-degree expect(health.hubs.length).toBeGreaterThan(0); expect(health.hubs[0].degree).toBeGreaterThan(0); }); it('computes composition coverage', () => { const engine = createEngine(); const health = engine.getHealth(); // 5 out of 6 components are in blocks (all except... let's check) // DashboardLayout: AppShell, Header, Sidebar; LoginForm: Dialog, Input, Button; SearchBar: Input, Button // All 6 components are in at least one block expect(health.compositionCoverage).toBe(100); }); it('computes average degree', () => { const engine = createEngine(); const health = engine.getHealth(); // 9 edges * 2 / 6 nodes = 3.0 expect(health.averageDegree).toBe(3); }); }); }); // --------------------------------------------------------------------------- // Health computation // --------------------------------------------------------------------------- describe('computeHealthFromData()', () => { it('identifies orphan nodes', () => { const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, { name: 'Orphan', category: 'test', status: 'stable' }, ]; const edges: GraphEdge[] = [ { source: 'A', target: 'B', type: 'imports', weight: 1.0, provenance: 'test' }, ]; const health = computeHealthFromData(nodes, edges); expect(health.orphans).toContain('Orphan'); expect(health.orphans).not.toContain('A'); expect(health.orphans).not.toContain('B'); }); it('handles empty graph', () => { const health = computeHealthFromData([], []); expect(health.nodeCount).toBe(0); expect(health.edgeCount).toBe(0); expect(health.orphans).toEqual([]); expect(health.hubs).toEqual([]); expect(health.averageDegree).toBe(0); expect(health.compositionCoverage).toBe(0); }); it('computes connected components correctly', () => { const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, { name: 'C', category: 'test', status: 'stable' }, { name: 'D', category: 'test', status: 'stable' }, ]; const edges: GraphEdge[] = [ { source: 'A', target: 'B', type: 'imports', weight: 1.0, provenance: 'test' }, { source: 'C', target: 'D', type: 'imports', weight: 1.0, provenance: 'test' }, ]; const health = computeHealthFromData(nodes, edges); expect(health.connectedComponents.length).toBe(2); }); it('limits hubs to top 10', () => { const nodes: ComponentNode[] = Array.from({ length: 15 }, (_, i) => ({ name: `C${i}`, category: 'test', status: 'stable', })); const edges: GraphEdge[] = Array.from({ length: 14 }, (_, i) => ({ source: 'C0', target: `C${i + 1}`, type: 'imports' as GraphEdgeType, weight: 1.0, provenance: 'test', })); const health = computeHealthFromData(nodes, edges); expect(health.hubs.length).toBe(10); expect(health.hubs[0].name).toBe('C0'); }); it('computes composition coverage with block index', () => { const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, { name: 'C', category: 'test', status: 'stable' }, { name: 'D', category: 'test', status: 'stable' }, ]; const blockIndex = new Map(); blockIndex.set('A', ['Block1']); blockIndex.set('B', ['Block1']); // C and D not in any block const health = computeHealthFromData(nodes, [], blockIndex); expect(health.compositionCoverage).toBe(50); }); }); // --------------------------------------------------------------------------- // Serialization round-trip // --------------------------------------------------------------------------- describe('serialization', () => { it('round-trips through serialize → deserialize', () => { const graph = createTestGraph(); const serialized = serializeGraph(graph); const deserialized = deserializeGraph(serialized); expect(deserialized.nodes).toEqual(graph.nodes); expect(deserialized.edges.length).toBe(graph.edges.length); expect(deserialized.health).toEqual(graph.health); }); it('preserves edge data', () => { const graph = createTestGraph(); const serialized = serializeGraph(graph); const deserialized = deserializeGraph(serialized); for (let i = 0; i < graph.edges.length; i++) { expect(deserialized.edges[i].source).toBe(graph.edges[i].source); expect(deserialized.edges[i].target).toBe(graph.edges[i].target); expect(deserialized.edges[i].type).toBe(graph.edges[i].type); expect(deserialized.edges[i].weight).toBe(graph.edges[i].weight); expect(deserialized.edges[i].provenance).toBe(graph.edges[i].provenance); } }); it('preserves optional note field', () => { const graph = createTestGraph(); const edgeWithNote = graph.edges.find(e => e.note); expect(edgeWithNote).toBeDefined(); const serialized = serializeGraph(graph); const deserialized = deserializeGraph(serialized); const matchingEdge = deserialized.edges.find( e => e.source === edgeWithNote!.source && e.target === edgeWithNote!.target && e.type === edgeWithNote!.type ); expect(matchingEdge?.note).toBe(edgeWithNote!.note); }); it('uses compact keys in serialized format', () => { const graph = createTestGraph(); const serialized = serializeGraph(graph); const edge = serialized.edges[0]; expect('s' in edge).toBe(true); expect('t' in edge).toBe(true); expect('ty' in edge).toBe(true); expect('w' in edge).toBe(true); expect('p' in edge).toBe(true); // Full keys should not be present expect('source' in edge).toBe(false); expect('target' in edge).toBe(false); }); it('omits note from serialized edge when undefined', () => { const edge: GraphEdge = { source: 'A', target: 'B', type: 'imports', weight: 1.0, provenance: 'test', }; const graph: ComponentGraph = { nodes: [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, ], edges: [edge], health: computeHealthFromData( [{ name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }], [edge] ), }; const serialized = serializeGraph(graph); expect(serialized.edges[0].no).toBeUndefined(); }); it('produces valid JSON', () => { const graph = createTestGraph(); const serialized = serializeGraph(graph); const json = JSON.stringify(serialized); const parsed = JSON.parse(json); expect(parsed.nodes.length).toBe(6); expect(parsed.edges.length).toBe(9); }); }); // --------------------------------------------------------------------------- // Edge cases // --------------------------------------------------------------------------- describe('edge cases', () => { it('engine handles graph with no edges', () => { const nodes: ComponentNode[] = [ { name: 'Lonely', category: 'test', status: 'stable' }, ]; const graph: ComponentGraph = { nodes, edges: [], health: computeHealthFromData(nodes, []), }; const engine = new ComponentGraphEngine(graph); expect(engine.dependencies('Lonely')).toEqual([]); expect(engine.dependents('Lonely')).toEqual([]); expect(engine.impact('Lonely').totalAffected).toBe(0); expect(engine.neighbors('Lonely').neighbors).toEqual([]); expect(engine.islands()).toEqual([['Lonely']]); }); it('engine handles single-node graph', () => { const nodes: ComponentNode[] = [ { name: 'Solo', category: 'test', status: 'stable' }, ]; const graph: ComponentGraph = { nodes, edges: [], health: computeHealthFromData(nodes, []), }; const engine = new ComponentGraphEngine(graph); expect(engine.getHealth().orphans).toEqual(['Solo']); expect(engine.getHealth().averageDegree).toBe(0); }); it('engine handles self-referencing edge gracefully', () => { const nodes: ComponentNode[] = [ { name: 'Self', category: 'test', status: 'stable' }, ]; const edges: GraphEdge[] = [ { source: 'Self', target: 'Self', type: 'renders', weight: 0.5, provenance: 'test' }, ]; const graph: ComponentGraph = { nodes, edges, health: computeHealthFromData(nodes, edges), }; const engine = new ComponentGraphEngine(graph); // Should not infinite-loop on impact const result = engine.impact('Self'); expect(result.totalAffected).toBe(0); // Can't affect self }); it('engine handles multiple edge types between same components', () => { const nodes: ComponentNode[] = [ { name: 'A', category: 'test', status: 'stable' }, { name: 'B', category: 'test', status: 'stable' }, ]; const edges: GraphEdge[] = [ { source: 'A', target: 'B', type: 'imports', weight: 1.0, provenance: 'test' }, { source: 'A', target: 'B', type: 'renders', weight: 0.5, provenance: 'test' }, { source: 'A', target: 'B', type: 'parent-of', weight: 1.0, provenance: 'test' }, ]; const graph: ComponentGraph = { nodes, edges, health: computeHealthFromData(nodes, edges), }; const engine = new ComponentGraphEngine(graph); expect(engine.dependencies('A').length).toBe(3); expect(engine.dependencies('A', ['imports']).length).toBe(1); }); it('composition with no parent returns undefined parent', () => { const engine = createEngine(); const result = engine.composition('AppShell'); expect(result.parent).toBeUndefined(); }); it('handles hasNode for all test components', () => { const engine = createEngine(); expect(engine.hasNode('Button')).toBe(true); expect(engine.hasNode('Dialog')).toBe(true); expect(engine.hasNode('Header')).toBe(true); expect(engine.hasNode('Sidebar')).toBe(true); expect(engine.hasNode('AppShell')).toBe(true); expect(engine.hasNode('Input')).toBe(true); expect(engine.hasNode('Tooltip')).toBe(false); }); });