apisurf
Version:
Analyze API surface changes between npm package versions to catch breaking changes
290 lines (289 loc) • 12.6 kB
JavaScript
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' }
]);
});
});