Skip to main content
Glama
ClusterVisualizer.test.ts8.01 kB
/** * Tests for ClusterVisualizer * * Verifies cluster boundary generation and color-coding. */ import { describe, it, expect, beforeEach } from '@jest/globals'; import { ClusterVisualizer, type Cluster } from '../../artifacts/visualizations/ClusterVisualizer.js'; import type { GraphNode } from '../../artifacts/visualizations/D3GraphRenderer.js'; describe('ClusterVisualizer', () => { let visualizer: ClusterVisualizer; let testClusters: Cluster[]; beforeEach(() => { testClusters = [ { id: 'cluster1', label: 'Combat Signals', nodeIds: ['signal1', 'signal2', 'signal3'], nodes: [ { id: 'signal1', label: 'damage_dealt', type: 'EventBus', x: 100, y: 100 }, { id: 'signal2', label: 'character_died', type: 'EventBus', x: 150, y: 150 }, { id: 'signal3', label: 'combat_started', type: 'EventBus', x: 200, y: 100 } ] }, { id: 'cluster2', label: 'UI Signals', nodeIds: ['signal4', 'signal5'], nodes: [ { id: 'signal4', label: 'button_clicked', type: 'SignalBus', x: 400, y: 200 }, { id: 'signal5', label: 'menu_opened', type: 'SignalBus', x: 450, y: 250 } ] } ]; visualizer = new ClusterVisualizer(testClusters); }); describe('Constructor', () => { it('should initialize with cluster data', () => { expect(visualizer).toBeDefined(); }); it('should handle empty cluster array', () => { const emptyVisualizer = new ClusterVisualizer([]); expect(emptyVisualizer).toBeDefined(); }); }); describe('generateClusterBoundaries', () => { it('should generate SVG path for cluster with 3+ nodes', () => { const boundaries = visualizer.generateClusterBoundaries(testClusters); expect(boundaries).toContain('<path'); expect(boundaries).toContain('cluster1'); expect(boundaries).toContain('d="M'); // SVG path command }); it('should skip clusters with < 3 nodes', () => { const smallCluster: Cluster = { id: 'small', label: 'Small', nodeIds: ['node1', 'node2'], nodes: [ { id: 'node1', label: 'A', type: 'EventBus', x: 10, y: 10 }, { id: 'node2', label: 'B', type: 'EventBus', x: 20, y: 20 } ] }; const boundaries = visualizer.generateClusterBoundaries([smallCluster]); // Should not contain path for small cluster expect(boundaries.trim()).toBe(''); }); it('should include cluster ID in path data attribute', () => { const boundaries = visualizer.generateClusterBoundaries(testClusters); // cluster1 has 3 nodes, cluster2 has only 2 (skipped) expect(boundaries).toContain('data-cluster-id="cluster1"'); expect(boundaries).not.toContain('data-cluster-id="cluster2"'); // <3 nodes }); it('should use semi-transparent fill', () => { const boundaries = visualizer.generateClusterBoundaries(testClusters); expect(boundaries).toContain('rgba('); expect(boundaries).toContain('0.1)'); // 10% opacity }); it('should use dashed stroke', () => { const boundaries = visualizer.generateClusterBoundaries(testClusters); expect(boundaries).toContain('stroke-dasharray="5,5"'); }); it('should handle nodes without positions', () => { const clustersWithMissingPos: Cluster[] = [ { id: 'test', label: 'Test', nodeIds: ['node1', 'node2', 'node3'], nodes: [ { id: 'node1', label: 'A', type: 'EventBus' }, // Missing x, y { id: 'node2', label: 'B', type: 'EventBus', x: 10, y: 10 }, { id: 'node3', label: 'C', type: 'EventBus', x: 20, y: 20 } ] } ]; const boundaries = visualizer.generateClusterBoundaries(clustersWithMissingPos); // Should still generate path (with 2 nodes) expect(boundaries).toBe(''); // Not enough nodes with positions }); }); describe('colorCodeNodes', () => { it('should assign colors to nodes based on cluster', () => { const nodes: GraphNode[] = [ { id: 'signal1', label: 'A', type: 'EventBus' }, { id: 'signal2', label: 'B', type: 'EventBus' }, { id: 'signal4', label: 'C', type: 'SignalBus' } ]; const colorMap = visualizer.colorCodeNodes(nodes, testClusters); expect(colorMap.size).toBe(3); expect(colorMap.get('signal1')).toBeTruthy(); // cluster1 color expect(colorMap.get('signal2')).toBeTruthy(); // cluster1 color expect(colorMap.get('signal4')).toBeTruthy(); // cluster2 color }); it('should use same color for nodes in same cluster', () => { const nodes: GraphNode[] = [ { id: 'signal1', label: 'A', type: 'EventBus' }, { id: 'signal2', label: 'B', type: 'EventBus' } ]; const colorMap = visualizer.colorCodeNodes(nodes, testClusters); // Both in cluster1, should have same color expect(colorMap.get('signal1')).toBe(colorMap.get('signal2')); }); it('should use different colors for different clusters', () => { const nodes: GraphNode[] = [ { id: 'signal1', label: 'A', type: 'EventBus' }, { id: 'signal4', label: 'B', type: 'SignalBus' } ]; const colorMap = visualizer.colorCodeNodes(nodes, testClusters); // Different clusters, different colors expect(colorMap.get('signal1')).not.toBe(colorMap.get('signal4')); }); it('should assign default color to unclustered nodes', () => { const nodes: GraphNode[] = [ { id: 'orphan', label: 'Orphan', type: 'EventBus' } ]; const colorMap = visualizer.colorCodeNodes(nodes, testClusters); expect(colorMap.get('orphan')).toBe('#999'); // Default gray }); it('should handle empty node array', () => { const colorMap = visualizer.colorCodeNodes([], testClusters); expect(colorMap.size).toBe(0); }); it('should handle empty cluster array', () => { const nodes: GraphNode[] = [ { id: 'node1', label: 'A', type: 'EventBus' } ]; const emptyVisualizer = new ClusterVisualizer([]); const colorMap = emptyVisualizer.colorCodeNodes(nodes, []); expect(colorMap.get('node1')).toBe('#999'); // All unclustered }); }); describe('getClusterColor', () => { it('should return color for valid cluster ID', () => { const color = visualizer.getClusterColor('cluster1'); expect(color).toBeTruthy(); expect(color).toMatch(/^#[0-9a-f]{6}$/i); // Hex color format }); it('should return default color for unknown cluster ID', () => { const color = visualizer.getClusterColor('unknown_cluster'); expect(color).toBe('#999'); }); }); describe('Convex Hull Algorithm', () => { it('should create closed SVG path (ends with Z)', () => { const boundaries = visualizer.generateClusterBoundaries(testClusters); // Paths should end with Z (close path) expect(boundaries).toContain('Z'); }); it('should handle collinear points', () => { const collinearCluster: Cluster = { id: 'collinear', label: 'Collinear', nodeIds: ['n1', 'n2', 'n3'], nodes: [ { id: 'n1', label: 'A', type: 'EventBus', x: 0, y: 0 }, { id: 'n2', label: 'B', type: 'EventBus', x: 10, y: 10 }, { id: 'n3', label: 'C', type: 'EventBus', x: 20, y: 20 } ] }; const boundaries = visualizer.generateClusterBoundaries([collinearCluster]); // Should still generate path expect(boundaries).toContain('<path'); }); it('should add padding around hull', () => { // This is implicitly tested by the expand hull logic // Explicit test would require parsing SVG path and checking coordinates const boundaries = visualizer.generateClusterBoundaries(testClusters); expect(boundaries.length).toBeGreaterThan(0); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EricA1019/CTS_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server