UNPKG

apisurf

Version:

Analyze API surface changes between npm package versions to catch breaking changes

290 lines (289 loc) 12.6 kB
import { describe, it, expect, jest, beforeEach } from '@jest/globals'; // Mock modules using ESM-compatible approach const mockFs = { existsSync: jest.fn(), readdirSync: jest.fn(), readFileSync: jest.fn(), lstatSync: jest.fn(), }; const mockPath = { join: jest.fn(), }; jest.unstable_mockModule('fs', () => mockFs); jest.unstable_mockModule('path', () => mockPath); // Import after mocking const { findPackagesToAnalyze } = await import('./findPackagesToAnalyze.js'); describe('findPackagesToAnalyze', () => { beforeEach(() => { jest.clearAllMocks(); // Mock process.cwd() to return a consistent value jest.spyOn(process, 'cwd').mockReturnValue('/test/project'); // Setup default path.join behavior mockPath.join.mockImplementation((...segments) => segments.join('/')); }); it('should return single package when packages directory does not exist', () => { // Mock packages directory doesn't exist mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return false; } if (filePath === 'package.json') { return true; } return false; }); // Mock package.json content const packageJson = { name: 'my-single-package', version: '1.0.0' }; mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === 'package.json') { return JSON.stringify(packageJson); } return ''; }); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: 'my-single-package', path: '.' } ]); }); it('should return empty array when neither packages directory nor package.json exists', () => { // Mock both packages directory and package.json don't exist mockFs.existsSync.mockReturnValue(false); const result = findPackagesToAnalyze(); expect(result).toEqual([]); }); it('should return package with fallback name when package.json has no name field', () => { // Mock packages directory doesn't exist mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return false; } if (filePath === 'package.json') { return true; } return false; }); // Mock package.json without name field const packageJson = { version: '1.0.0' // No name field }; mockFs.readFileSync.mockReturnValue(JSON.stringify(packageJson)); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: 'package', path: '.' } ]); }); it('should find multiple packages in monorepo structure', () => { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } // Mock package.json files exist for all packages if (filePath.endsWith('package.json')) { return true; } return false; }); // Mock directory listing mockFs.readdirSync.mockReturnValue(['package-a', 'package-b', 'package-c']); // Mock package.json contents for each package mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages/package-a/package.json') { return JSON.stringify({ name: '@scope/package-a', version: '1.0.0' }); } if (filePath === '/test/project/packages/package-b/package.json') { return JSON.stringify({ name: 'package-b', version: '2.0.0' }); } if (filePath === '/test/project/packages/package-c/package.json') { return JSON.stringify({ name: 'package-c', version: '1.5.0' }); } return ''; }); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: '@scope/package-a', path: '/test/project/packages/package-a' }, { name: 'package-b', path: '/test/project/packages/package-b' }, { name: 'package-c', path: '/test/project/packages/package-c' } ]); }); it('should handle packages with missing package.json files in monorepo', () => { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } // Only package-a has package.json if (filePath === '/test/project/packages/package-a/package.json') { return true; } if (filePath === '/test/project/packages/package-b/package.json') { return false; } if (filePath === '/test/project/packages/not-a-package/package.json') { return false; } return false; }); // Mock directory listing mockFs.readdirSync.mockReturnValue(['package-a', 'package-b', 'not-a-package']); // Mock package.json content for package-a only mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages/package-a/package.json') { return JSON.stringify({ name: 'package-a', version: '1.0.0' }); } return ''; }); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: 'package-a', path: '/test/project/packages/package-a' } ]); }); it('should use directory name as fallback when package.json has no name in monorepo', () => { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } if (filePath.endsWith('package.json')) { return true; } return false; }); // Mock directory listing mockFs.readdirSync.mockReturnValue(['unnamed-package']); // Mock package.json without name field mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages/unnamed-package/package.json') { return JSON.stringify({ version: '1.0.0' }); // No name field } return ''; }); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: 'unnamed-package', path: '/test/project/packages/unnamed-package' } ]); }); it('should handle invalid JSON in package.json files gracefully', () => { // Suppress console.warn for this test since we're intentionally testing error handling const originalWarn = console.warn; console.warn = jest.fn(); try { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } if (filePath.endsWith('package.json')) { return true; } return false; }); // Mock directory listing mockFs.readdirSync.mockReturnValue(['good-package', 'bad-package']); // Mock package.json contents - one valid, one invalid mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages/good-package/package.json') { return JSON.stringify({ name: 'good-package', version: '1.0.0' }); } if (filePath === '/test/project/packages/bad-package/package.json') { return '{ invalid json }'; // Invalid JSON } return ''; }); // The function should handle the JSON.parse error and only return valid packages const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: 'good-package', path: '/test/project/packages/good-package' } ]); // Verify that the warning was called (optional - confirms error handling works) expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Warning: Could not parse package.json'), expect.any(Error)); } finally { // Always restore console.warn console.warn = originalWarn; } }); it('should return empty array when packages directory exists but is empty', () => { // Mock packages directory exists but is empty mockFs.existsSync.mockImplementation((filePath) => { return filePath === '/test/project/packages'; }); // Mock empty directory listing mockFs.readdirSync.mockReturnValue([]); const result = findPackagesToAnalyze(); expect(result).toEqual([]); }); it('should work with complex package names and paths', () => { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } if (filePath.endsWith('package.json')) { return true; } return false; }); // Mock directory listing with various directory name patterns mockFs.readdirSync.mockReturnValue(['my-package', 'another_package', 'package.with.dots']); // Mock package.json contents mockFs.readFileSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages/my-package/package.json') { return JSON.stringify({ name: '@my-org/my-package', version: '1.0.0' }); } if (filePath === '/test/project/packages/another_package/package.json') { return JSON.stringify({ name: 'another_package', version: '2.0.0' }); } if (filePath === '/test/project/packages/package.with.dots/package.json') { return JSON.stringify({ name: '@weird/package.with.dots', version: '3.0.0' }); } return ''; }); const result = findPackagesToAnalyze(); expect(result).toEqual([ { name: '@my-org/my-package', path: '/test/project/packages/my-package' }, { name: 'another_package', path: '/test/project/packages/another_package' }, { name: '@weird/package.with.dots', path: '/test/project/packages/package.with.dots' } ]); }); it('should handle file system errors gracefully', () => { // Mock packages directory exists mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/test/project/packages') { return true; } return false; }); // Mock readdirSync to throw an error mockFs.readdirSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Should handle the error and return empty array expect(() => findPackagesToAnalyze()).toThrow('Permission denied'); }); it('should respect the current working directory', () => { // Change the current working directory jest.spyOn(process, 'cwd').mockReturnValue('/different/project'); // Mock packages directory exists in the new location mockFs.existsSync.mockImplementation((filePath) => { if (filePath === '/different/project/packages') { return true; } if (filePath.endsWith('package.json')) { return true; } return false; }); mockFs.readdirSync.mockReturnValue(['test-package']); mockFs.readFileSync.mockReturnValue(JSON.stringify({ name: 'test-package', version: '1.0.0' })); const result = findPackagesToAnalyze(); expect(mockPath.join).toHaveBeenCalledWith('/different/project', 'packages'); expect(result).toEqual([ { name: 'test-package', path: '/different/project/packages/test-package' } ]); }); });