UNPKG

filetree-pro

Version:

A powerful file tree generator for VS Code and Cursor. Generate beautiful file trees in multiple formats with smart exclusions and custom configurations.

348 lines (282 loc) 11.6 kB
/** * Test Suite: TreeBuilderService * Tests file tree building logic with proper method signatures * * @author FileTree Pro Team * @since 0.3.0 */ import * as vscode from 'vscode'; import { ExclusionService } from '../services/exclusionService'; import { TreeBuilderService } from '../services/treeBuilderService'; describe('TreeBuilderService', () => { let service: TreeBuilderService; let mockExclusionService: jest.Mocked<ExclusionService>; beforeEach(() => { // Create mock ExclusionService mockExclusionService = { shouldExclude: jest.fn().mockReturnValue(false), readGitignore: jest.fn().mockResolvedValue(['node_modules/', 'dist/', '*.log']), dispose: jest.fn(), } as any; // Create service instance service = new TreeBuilderService(mockExclusionService); // Reset VS Code mocks jest.clearAllMocks(); // Setup default VS Code mocks (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue([ ['src', vscode.FileType.Directory], ['package.json', vscode.FileType.File], ['README.md', vscode.FileType.File], ]); }); describe('buildFileTreeItems', () => { test('should build tree items with correct parameters', async () => { const rootPath = '/test/project'; const maxDepth = 10; const depth = 0; const items = await service.buildFileTreeItems(rootPath, maxDepth, rootPath, depth); expect(items).toHaveLength(3); expect(items[0].name).toBe('src'); expect(items[0].type).toBe('folder'); // Files are sorted alphabetically expect(items[1].name).toBe('README.md'); expect(items[1].type).toBe('file'); expect(items[2].name).toBe('package.json'); expect(items[2].type).toBe('file'); }); test('should call readGitignore on first depth level', async () => { const rootPath = '/test/project'; await service.buildFileTreeItems(rootPath, 10, rootPath, 0); expect(mockExclusionService.readGitignore).toHaveBeenCalledWith(rootPath); expect(mockExclusionService.readGitignore).toHaveBeenCalledTimes(1); }); test('should not call readGitignore on deeper levels', async () => { const rootPath = '/test/project'; await service.buildFileTreeItems(rootPath, 10, rootPath, 2); // depth = 2 expect(mockExclusionService.readGitignore).not.toHaveBeenCalled(); }); test('should respect maxDepth limit', async () => { const rootPath = '/test/project'; const maxDepth = 1; const items = await service.buildFileTreeItems(rootPath, maxDepth, rootPath, 2); // depth > maxDepth expect(items).toHaveLength(0); expect(vscode.workspace.fs.readDirectory).not.toHaveBeenCalled(); }); test('should exclude items based on exclusion service', async () => { mockExclusionService.shouldExclude.mockImplementation( (name: string) => name === 'node_modules' ); (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue([ ['node_modules', vscode.FileType.Directory], ['src', vscode.FileType.Directory], ['package.json', vscode.FileType.File], ]); const items = await service.buildFileTreeItems('/test/project', 10, '/test/project', 0); // Should have 2 items (node_modules excluded) expect(items).toHaveLength(2); expect(items.find(item => item.name === 'node_modules')).toBeUndefined(); expect(items.find(item => item.name === 'src')).toBeDefined(); }); test('should sort folders before files', async () => { (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue([ ['zebra.txt', vscode.FileType.File], ['apple-folder', vscode.FileType.Directory], ['banana.txt', vscode.FileType.File], ['zebra-folder', vscode.FileType.Directory], ]); const items = await service.buildFileTreeItems('/test/project', 10, '/test/project', 0); // First two should be folders (alphabetically sorted) expect(items[0].type).toBe('folder'); expect(items[0].name).toBe('apple-folder'); expect(items[1].type).toBe('folder'); expect(items[1].name).toBe('zebra-folder'); // Next two should be files (alphabetically sorted) expect(items[2].type).toBe('file'); expect(items[2].name).toBe('banana.txt'); expect(items[3].type).toBe('file'); expect(items[3].name).toBe('zebra.txt'); }); test('should handle progress callback', async () => { const progressCallback = jest.fn(); await service.buildFileTreeItems('/test/project', 10, '/test/project', 0, progressCallback); expect(progressCallback).toHaveBeenCalledWith(expect.stringContaining('Building tree')); }); test('should handle cancellation token', async () => { const mockToken = { isCancellationRequested: true, onCancellationRequested: jest.fn(), } as any; const items = await service.buildFileTreeItems( '/test/project', 10, '/test/project', 0, undefined, mockToken ); expect(items).toHaveLength(0); expect(vscode.workspace.fs.readDirectory).not.toHaveBeenCalled(); }); test('should handle errors gracefully', async () => { (vscode.workspace.fs.readDirectory as jest.Mock).mockRejectedValue( new Error('Permission denied') ); const items = await service.buildFileTreeItems('/test/project', 10, '/test/project', 0); expect(items).toHaveLength(0); // Returns empty array on error }); test('should build nested folder structure', async () => { // Setup nested structure (vscode.workspace.fs.readDirectory as jest.Mock) .mockResolvedValueOnce([ ['src', vscode.FileType.Directory], ['package.json', vscode.FileType.File], ]) .mockResolvedValueOnce([ ['utils.ts', vscode.FileType.File], ['index.ts', vscode.FileType.File], ]); const items = await service.buildFileTreeItems('/test/project', 10, '/test/project', 0); expect(items).toHaveLength(2); expect(items[0].name).toBe('src'); expect(items[0].children).toHaveLength(2); expect(items[0].children![0].name).toBe('index.ts'); expect(items[0].children![1].name).toBe('utils.ts'); }); }); describe('generateTreeLines', () => { test('should generate ASCII tree lines with correct parameters', async () => { const lines: string[] = []; const rootPath = '/test/project'; const prefix = ''; const depth = 0; const maxDepth = 10; const showIcons = true; await service.generateTreeLines( rootPath, prefix, lines, depth, maxDepth, showIcons, rootPath ); expect(lines.length).toBeGreaterThan(0); expect(mockExclusionService.readGitignore).toHaveBeenCalledWith(rootPath); }); test('should include icons when showIcons is true', async () => { const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 0, 10, true, '/test/project'); // Should have folder icon (📁) for folders const folderLines = lines.filter(line => line.includes('📁')); expect(folderLines.length).toBeGreaterThan(0); }); test('should exclude icons when showIcons is false', async () => { const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 0, 10, false, '/test/project'); // Should NOT have folder icon for folders const folderLines = lines.filter(line => line.includes('📁')); expect(folderLines.length).toBe(0); }); test('should use correct tree connectors', async () => { (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue([ ['file1.txt', vscode.FileType.File], ['file2.txt', vscode.FileType.File], ]); const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 0, 10, false, '/test/project'); // First item should use ├──, last should use └── expect(lines[0]).toContain('├──'); expect(lines[lines.length - 1]).toContain('└──'); }); test('should respect maxDepth in tree lines', async () => { const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 5, 3, true, '/test/project'); // Should not process anything because depth (5) > maxDepth (3) expect(lines).toHaveLength(0); }); test('should handle progress callback in tree lines', async () => { const progressCallback = jest.fn(); const lines: string[] = []; await service.generateTreeLines( '/test/project', '', lines, 0, 10, true, '/test/project', progressCallback ); expect(progressCallback).toHaveBeenCalledWith(expect.stringContaining('Reading directory')); }); test('should handle cancellation in tree lines', async () => { const mockToken = { isCancellationRequested: true, onCancellationRequested: jest.fn(), } as any; const lines: string[] = []; await service.generateTreeLines( '/test/project', '', lines, 0, 10, true, '/test/project', undefined, mockToken ); expect(lines).toHaveLength(0); expect(vscode.workspace.fs.readDirectory).not.toHaveBeenCalled(); }); test('should handle errors in tree lines', async () => { (vscode.workspace.fs.readDirectory as jest.Mock).mockRejectedValue( new Error('Permission denied') ); const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 0, 10, true, '/test/project'); // Should add error line expect(lines.length).toBeGreaterThan(0); expect(lines[0]).toContain('Error reading directory'); }); test('should process large directories in batches', async () => { // Create a large array of items (> 100 items to trigger batching) const largeArray = Array.from({ length: 150 }, (_, i) => [ `file${i}.txt`, vscode.FileType.File, ]); (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue(largeArray); const progressCallback = jest.fn(); const lines: string[] = []; await service.generateTreeLines( '/test/project', '', lines, 0, 10, true, '/test/project', progressCallback ); // Should have processed all items expect(lines.length).toBe(150); // Progress callback should have been called for batch progress expect(progressCallback).toHaveBeenCalled(); }); test('should exclude items in tree lines', async () => { mockExclusionService.shouldExclude.mockImplementation( (name: string) => name === 'secret.txt' ); (vscode.workspace.fs.readDirectory as jest.Mock).mockResolvedValue([ ['public.txt', vscode.FileType.File], ['secret.txt', vscode.FileType.File], ]); const lines: string[] = []; await service.generateTreeLines('/test/project', '', lines, 0, 10, false, '/test/project'); // Should only have 1 line (secret.txt excluded) expect(lines).toHaveLength(1); expect(lines[0]).toContain('public.txt'); expect(lines.find(line => line.includes('secret.txt'))).toBeUndefined(); }); }); });