UNPKG

ruch

Version:

Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.

375 lines (309 loc) 10.9 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs-extra'; import path from 'path'; import { executeVisualize, type VisualizeOptions } from '../../src/commands/visualize'; import { generateGraphData } from '../../src/utils/dependency-analyzer'; import { createFileSystem, type FileSystem } from '../../src/utils/file-operations'; import { logger } from '../../src/utils/logger'; // Mock des dépendances vi.mock('../../src/utils/dependency-analyzer'); vi.mock('../../src/utils/file-operations'); vi.mock('../../src/utils/logger', () => ({ logger: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn(), log: vi.fn(), } })); vi.mock('fs-extra'); vi.mock('fs'); vi.mock('express'); vi.mock('child_process'); const mockGenerateGraphData = vi.mocked(generateGraphData); const mockCreateFileSystem = vi.mocked(createFileSystem); const mockLogger = vi.mocked(logger); const mockFs = vi.mocked(fs); // Mock fs const mockWriteFile = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const mockFsNode = vi.hoisted(() => ({ default: { promises: { writeFile: mockWriteFile } }, promises: { writeFile: mockWriteFile } })); vi.mock('fs', () => mockFsNode); // Mock console methods const consoleSpy = { log: vi.spyOn(console, 'log').mockImplementation(() => {}), error: vi.spyOn(console, 'error').mockImplementation(() => {}) }; // Mock data pour les tests const mockGraphData = { nodes: [ { id: 'user', name: 'user', type: 'feature', fileCount: 5, dependencies: ['shared'], dependents: [], files: ['user/entities/User.ts', 'user/services/UserService.ts'] }, { id: 'shared', name: 'shared', type: 'shared', fileCount: 3, dependencies: [], dependents: ['user'], files: ['shared/utils/helpers.ts'] } ], edges: [ { from: 'user', to: 'shared', type: 'direct-import' as const, count: 1, metadata: { files: ['user/services/UserService.ts'] } } ], circularDependencies: [], stats: { totalDomains: 2, totalDependencies: 1, circularDependencyCount: 0 } }; const mockGraphDataWithCircularDeps = { ...mockGraphData, circularDependencies: [['user', 'product']], stats: { ...mockGraphData.stats, circularDependencyCount: 1 } }; describe('Visualize Command', () => { let mockFileSystem: FileSystem; beforeEach(() => { // Reset tous les mocks vi.clearAllMocks(); // Reset mock fs mockWriteFile.mockClear(); mockWriteFile.mockResolvedValue(undefined); // Setup mock FileSystem mockFileSystem = { writeFile: vi.fn() as any, readFile: vi.fn() as any, readdir: vi.fn() as any, stat: vi.fn() as any, remove: vi.fn() as any, mkdir: vi.fn() as any, exists: vi.fn() as any, pathExists: vi.fn() as any, ensureDir: vi.fn() as any }; mockCreateFileSystem.mockReturnValue(mockFileSystem); mockGenerateGraphData.mockResolvedValue(mockGraphData); // Mock des méthodes du logger mockLogger.info = vi.fn(); mockLogger.success = vi.fn(); mockLogger.warning = vi.fn(); mockLogger.error = vi.fn(); }); afterEach(() => { consoleSpy.log.mockClear(); consoleSpy.error.mockClear(); }); describe('executeVisualize - format console', () => { it('should display console visualization', async () => { const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await executeVisualize(options); expect(mockGenerateGraphData).toHaveBeenCalledWith('/test-project', mockFileSystem); expect(mockLogger.info).toHaveBeenCalledWith('🔍 Analyzing domain dependencies...'); expect(mockLogger.success).toHaveBeenCalledWith('Found 2 domains with 1 dependencies'); // Vérifier que la sortie console a été appelée expect(consoleSpy.log).toHaveBeenCalledWith('\n🏗️ RUCH Domain Visualization\n'); expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining('Domains: 2')); expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining('Dependencies: 1')); }); it('should warn about circular dependencies in console', async () => { mockGenerateGraphData.mockResolvedValue(mockGraphDataWithCircularDeps); const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await executeVisualize(options); expect(mockLogger.warning).toHaveBeenCalledWith('⚠️ Found 1 circular dependencies'); // La sortie console est générée par la fonction displayConsoleVisualization expect(consoleSpy.log).toHaveBeenCalled(); }); it('should handle empty project gracefully', async () => { const emptyGraphData = { nodes: [], edges: [], circularDependencies: [], stats: { totalDomains: 0, totalDependencies: 0, circularDependencyCount: 0 } }; mockGenerateGraphData.mockResolvedValue(emptyGraphData); const options: VisualizeOptions = { format: 'console', projectRoot: '/empty-project' }; await executeVisualize(options); expect(mockLogger.warning).toHaveBeenCalledWith('No domains found in src/domains/. Make sure you have domains created.'); }); }); describe('executeVisualize - format json', () => { it('should save graph data to JSON file', async () => { const options: VisualizeOptions = { format: 'json', output: '/test-project/custom-output.json', projectRoot: '/test-project' }; await executeVisualize(options); expect(mockGenerateGraphData).toHaveBeenCalledWith('/test-project', mockFileSystem); expect(mockWriteFile).toHaveBeenCalledWith( '/test-project/custom-output.json', JSON.stringify(mockGraphData, null, 2) ); expect(mockLogger.success).toHaveBeenCalledWith('Graph data saved to: /test-project/custom-output.json'); }); it('should use default output path if not specified', async () => { const options: VisualizeOptions = { format: 'json', projectRoot: '/test-project' }; await executeVisualize(options); expect(mockWriteFile).toHaveBeenCalledWith( '/test-project/graph-data.json', JSON.stringify(mockGraphData, null, 2) ); expect(mockLogger.success).toHaveBeenCalledWith('Graph data saved to: /test-project/graph-data.json'); }); }); describe('executeVisualize - format app', () => { it('should handle app format initialization', async () => { const options: VisualizeOptions = { format: 'app', port: 3001, projectRoot: '/test-project', open: false }; // Mock spawn pour le build process const { spawn } = await import('child_process'); const mockSpawn = vi.mocked(spawn); const mockBuildProcess = { stdout: { on: vi.fn() }, stderr: { on: vi.fn() }, on: vi.fn((event, callback) => { if (event === 'close') { callback(0); // Success exit code } }) }; mockSpawn.mockReturnValue(mockBuildProcess as any); // Mock express const mockExpress = await import('express'); const mockApp = { get: vi.fn(), use: vi.fn(), listen: vi.fn((port, callback) => { callback(); return { on: vi.fn(), close: vi.fn() }; }) }; vi.mocked(mockExpress.default).mockReturnValue(mockApp as any); await executeVisualize(options); expect(mockLogger.info).toHaveBeenCalledWith('📦 Building React app...'); expect(mockLogger.success).toHaveBeenCalledWith('✅ React app built successfully'); expect(mockLogger.info).toHaveBeenCalledWith('🚀 Starting visualization server...'); }); }); describe('executeVisualize - error handling', () => { it('should handle analysis errors gracefully', async () => { const error = new Error('Analysis failed'); mockGenerateGraphData.mockRejectedValue(error); const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await expect(executeVisualize(options)).rejects.toThrow('Analysis failed'); }); it('should handle file system errors', async () => { const options: VisualizeOptions = { format: 'json', output: '/invalid/path/output.json', projectRoot: '/test-project' }; // Mock une erreur de fs pour ce test mockWriteFile.mockRejectedValueOnce(new Error('Permission denied')); await expect(executeVisualize(options)).rejects.toThrow('Permission denied'); }); }); describe('executeVisualize - options validation', () => { it('should use default options when not provided', async () => { const options: VisualizeOptions = {}; await executeVisualize(options); expect(mockGenerateGraphData).toHaveBeenCalledWith(process.cwd(), mockFileSystem); }); it('should handle custom project root', async () => { const options: VisualizeOptions = { projectRoot: '/custom/project/path', format: 'console' }; await executeVisualize(options); expect(mockGenerateGraphData).toHaveBeenCalledWith('/custom/project/path', mockFileSystem); }); it('should parse port correctly', async () => { const options: VisualizeOptions = { port: 4000, format: 'console' }; await executeVisualize(options); // Port should be parsed but not used in console format expect(mockGenerateGraphData).toHaveBeenCalled(); }); }); describe('Display functions', () => { it('should display domains information correctly', async () => { const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await executeVisualize(options); // Vérifier que la sortie console a été appelée expect(consoleSpy.log).toHaveBeenCalled(); }); it('should display statistics correctly', async () => { const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await executeVisualize(options); expect(consoleSpy.log).toHaveBeenCalled(); }); it('should show tips at the end', async () => { const options: VisualizeOptions = { format: 'console', projectRoot: '/test-project' }; await executeVisualize(options); expect(consoleSpy.log).toHaveBeenCalled(); }); }); });