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
text/typescript
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();
});
});
});