UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

322 lines (250 loc) 10.1 kB
/** * @fileoverview Tests for FileWatcher */ import { EventEmitter } from 'events'; import path from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { FileChangeEvent, FileChangeType, FileWatcher } from './file-watcher.js'; // Mock fs module vi.mock('fs', () => ({ watch: vi.fn(() => ({ close: vi.fn(), on: vi.fn() })) })); // Mock utils vi.mock('../utils/fs.js', () => ({ fileExists: vi.fn(() => Promise.resolve(true)), readFile: vi.fn(() => Promise.resolve('')) })); vi.mock('../utils/index.js', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), success: vi.fn() } })); describe('FileWatcher', () => { let fileWatcher: FileWatcher; let mockWatch: any; beforeEach(() => { vi.clearAllMocks(); mockWatch = vi.fn(() => ({ close: vi.fn(), on: vi.fn() })); vi.doMock('fs', () => ({ watch: mockWatch })); }); afterEach(async () => { if (fileWatcher) { await fileWatcher.stop(); } }); describe('constructor', () => { it('should create FileWatcher with default options', () => { fileWatcher = new FileWatcher({ watchDir: '/test' }); expect(fileWatcher).toBeInstanceOf(FileWatcher); expect(fileWatcher).toBeInstanceOf(EventEmitter); }); it('should merge provided options with defaults', () => { const options = { watchDir: '/custom', patterns: ['*.custom'], debounceMs: 200 }; fileWatcher = new FileWatcher(options); expect(fileWatcher).toBeInstanceOf(FileWatcher); }); }); describe('start', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test' }); }); it('should start watching successfully', async () => { await fileWatcher.start(); expect(mockWatch).toHaveBeenCalled(); }); it('should not start if already watching', async () => { await fileWatcher.start(); await fileWatcher.start(); // Second call should be ignored expect(mockWatch).toHaveBeenCalledTimes(1); }); it('should throw error if watch directory does not exist', async () => { const { fileExists } = await import('../utils/fs.js'); vi.mocked(fileExists).mockResolvedValueOnce(false); await expect(fileWatcher.start()).rejects.toThrow('Watch directory does not exist'); }); }); describe('stop', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test' }); }); it('should stop watching successfully', async () => { const mockWatcher = { close: vi.fn(), on: vi.fn() }; mockWatch.mockReturnValue(mockWatcher); await fileWatcher.start(); await fileWatcher.stop(); expect(mockWatcher.close).toHaveBeenCalled(); }); it('should not stop if not watching', async () => { await fileWatcher.stop(); // Should not throw }); }); describe('dependency tracking', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test' }); }); it('should add dependency relationships', () => { const dependent = '/test/component.ordo'; const dependency = '/test/utils.ts'; fileWatcher.addDependency(dependent, dependency); const depInfo = fileWatcher.getDependencyInfo(dependency); expect(depInfo?.dependents.has(path.resolve(dependent))).toBe(true); const dependentInfo = fileWatcher.getDependencyInfo(dependent); expect(dependentInfo?.dependencies.has(path.resolve(dependency))).toBe(true); }); it('should remove dependency relationships', () => { const dependent = '/test/component.ordo'; const dependency = '/test/utils.ts'; fileWatcher.addDependency(dependent, dependency); fileWatcher.removeDependency(dependent, dependency); const depInfo = fileWatcher.getDependencyInfo(dependency); expect(depInfo?.dependents.has(path.resolve(dependent))).toBe(false); const dependentInfo = fileWatcher.getDependencyInfo(dependent); expect(dependentInfo?.dependencies.has(path.resolve(dependency))).toBe(false); }); it('should get affected files including dependents', () => { const file1 = '/test/utils.ts'; const file2 = '/test/component.ordo'; const file3 = '/test/page.ordo'; // Set up dependency chain: file1 <- file2 <- file3 fileWatcher.addDependency(file2, file1); fileWatcher.addDependency(file3, file2); const affected = fileWatcher.getAffectedFiles(file1); expect(affected).toContain(path.resolve(file1)); expect(affected).toContain(path.resolve(file2)); expect(affected).toContain(path.resolve(file3)); }); }); describe('file pattern matching', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test', patterns: ['**/*.ordo', '**/*.ts'], ignorePatterns: ['**/node_modules/**', '**/dist/**'] }); }); it('should match included patterns', () => { // Access private method for testing const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher); expect(shouldWatch('/test/component.ordo')).toBe(true); expect(shouldWatch('/test/utils.ts')).toBe(true); expect(shouldWatch('/test/nested/component.ordo')).toBe(true); }); it('should ignore excluded patterns', () => { const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher); expect(shouldWatch('/test/node_modules/package.json')).toBe(false); expect(shouldWatch('/test/dist/bundle.js')).toBe(false); }); it('should not match non-included patterns', () => { const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher); expect(shouldWatch('/test/readme.md')).toBe(false); expect(shouldWatch('/test/image.png')).toBe(false); }); }); describe('change event handling', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test', debounceMs: 10 }); }); it('should emit change events for file modifications', async () => { const changePromise = new Promise<FileChangeEvent>((resolve) => { fileWatcher.once('change', resolve); }); // Simulate file change const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher); handleChange('change', '/test/component.ordo'); // Wait for debounce await new Promise(resolve => setTimeout(resolve, 20)); const changeEvent = await changePromise; expect(changeEvent.type).toBe(FileChangeType.CHANGED); expect(changeEvent.filePath).toBe(path.resolve('/test/component.ordo')); expect(changeEvent.extension).toBe('.ordo'); }); it('should debounce rapid file changes', async () => { const changes: FileChangeEvent[] = []; fileWatcher.on('change', (event) => changes.push(event)); const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher); // Simulate rapid changes handleChange('change', '/test/component.ordo'); handleChange('change', '/test/component.ordo'); handleChange('change', '/test/component.ordo'); // Wait for debounce await new Promise(resolve => setTimeout(resolve, 20)); // Should only emit one change event expect(changes).toHaveLength(1); }); it('should handle file deletion events', async () => { const { fileExists } = await import('../utils/fs.js'); vi.mocked(fileExists).mockResolvedValueOnce(false); const changePromise = new Promise<FileChangeEvent>((resolve) => { fileWatcher.once('change', resolve); }); const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher); handleChange('rename', '/test/component.ordo'); await new Promise(resolve => setTimeout(resolve, 20)); const changeEvent = await changePromise; expect(changeEvent.type).toBe(FileChangeType.DELETED); }); it('should handle file addition events', async () => { const { fileExists } = await import('../utils/fs.js'); vi.mocked(fileExists).mockResolvedValueOnce(true); const changePromise = new Promise<FileChangeEvent>((resolve) => { fileWatcher.once('change', resolve); }); const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher); handleChange('rename', '/test/new-component.ordo'); await new Promise(resolve => setTimeout(resolve, 20)); const changeEvent = await changePromise; expect(changeEvent.type).toBe(FileChangeType.ADDED); }); }); describe('error handling', () => { beforeEach(() => { fileWatcher = new FileWatcher({ watchDir: '/test' }); }); it('should emit error events from watcher', async () => { const mockWatcher = { close: vi.fn(), on: vi.fn() }; mockWatch.mockReturnValue(mockWatcher); await fileWatcher.start(); const errorPromise = new Promise<Error>((resolve) => { fileWatcher.once('error', resolve); }); // Simulate watcher error const errorCallback = mockWatcher.on.mock.calls.find(call => call[0] === 'error')?.[1]; const testError = new Error('Watcher error'); errorCallback?.(testError); const error = await errorPromise; expect(error).toBe(testError); }); it('should handle errors during file change processing', async () => { const { fileExists } = await import('../utils/fs.js'); vi.mocked(fileExists).mockRejectedValueOnce(new Error('File system error')); const errorPromise = new Promise<Error>((resolve) => { fileWatcher.once('error', resolve); }); const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher); handleChange('change', '/test/component.ordo'); await new Promise(resolve => setTimeout(resolve, 20)); const error = await errorPromise; expect(error).toBeInstanceOf(Error); }); }); });