UNPKG

apisurf

Version:

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

318 lines (317 loc) 14.7 kB
import { describe, it, expect } from '@jest/globals'; import { compareApiSurfaces } from './compareApiSurfaces.js'; describe('Function Argument Comparison', () => { describe('Arrow Functions', () => { it('should detect no breaking change when arguments are identical', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string) => ...', parameters: ['name: string'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string) => ...', parameters: ['name: string'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(0); expect(result.nonBreakingChanges.length).toBe(0); }); it('should detect non-breaking change when optional argument is added', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string) => ...', parameters: ['name: string'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string, prefix?: string) => ...', parameters: ['name: string', 'prefix?: string'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(0); expect(result.nonBreakingChanges.length).toBe(1); expect(result.nonBreakingChanges[0].type).toBe('parameter-added'); expect(result.nonBreakingChanges[0].description).toContain('added optional parameters'); }); it('should detect breaking change when required argument is added', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string) => ...', parameters: ['name: string'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['greet']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['greet', { name: 'greet', kind: 'function', signature: 'export const greet = (name: string, prefix: string) => ...', parameters: ['name: string', 'prefix: string'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(1); expect(result.breakingChanges[0].type).toBe('parameter-changed'); expect(result.breakingChanges[0].description).toContain('added required parameters'); expect(result.nonBreakingChanges.length).toBe(0); }); it('should detect breaking change when argument type changes', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['calculate']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['calculate', { name: 'calculate', kind: 'function', signature: 'export const calculate = (value: number) => ...', parameters: ['value: number'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['calculate']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['calculate', { name: 'calculate', kind: 'function', signature: 'export const calculate = (value: string) => ...', parameters: ['value: string'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(1); expect(result.breakingChanges[0].type).toBe('parameter-changed'); expect(result.breakingChanges[0].description).toContain('changed parameter'); expect(result.breakingChanges[0].before).toBe('value: number'); expect(result.breakingChanges[0].after).toBe('value: string'); }); it('should detect breaking change when argument is removed', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['process']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['process', { name: 'process', kind: 'function', signature: 'export const process = (data: string, options: Options) => ...', parameters: ['data: string', 'options: Options'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['process']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['process', { name: 'process', kind: 'function', signature: 'export const process = (data: string) => ...', parameters: ['data: string'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(1); expect(result.breakingChanges[0].type).toBe('parameter-changed'); expect(result.breakingChanges[0].description).toContain('removed parameters'); }); }); describe('Function Type Aliases', () => { it('should detect breaking change when function type signature changes', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(), typeOnlyExports: new Set(['Handler']), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['Handler', { name: 'Handler', kind: 'type', signature: 'export type Handler = (event: Event) => void;' }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(), typeOnlyExports: new Set(['Handler']), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['Handler', { name: 'Handler', kind: 'type', signature: 'export type Handler = (event: Event, context: Context) => void;' }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); expect(result.breakingChanges.length).toBe(1); expect(result.breakingChanges[0].type).toBe('type-changed'); expect(result.breakingChanges[0].description).toContain('Changed type'); }); }); describe('Complex Scenarios', () => { it('should handle functions with multiple parameter changes', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['complexFunc']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['complexFunc', { name: 'complexFunc', kind: 'function', signature: 'export const complexFunc = (a: string, b: number) => ...', parameters: ['a: string', 'b: number'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['complexFunc']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['complexFunc', { name: 'complexFunc', kind: 'function', signature: 'export const complexFunc = (a: string, b: string, c?: boolean) => ...', parameters: ['a: string', 'b: string', 'c?: boolean'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); // Should detect breaking change because parameter 'b' changed type from number to string expect(result.breakingChanges.length).toBeGreaterThan(0); const typeChange = result.breakingChanges.find(c => c.description.includes('changed parameter')); expect(typeChange).toBeDefined(); }); it('should handle default parameters as optional', () => { const baseSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['withDefaults']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['withDefaults', { name: 'withDefaults', kind: 'function', signature: 'export const withDefaults = (name: string) => ...', parameters: ['name: string'] }] ]) }; const currentSurface = { packageName: 'test-pkg', version: '1.0.0', namedExports: new Set(['withDefaults']), typeOnlyExports: new Set(), defaultExport: false, starExports: [], typeDefinitions: new Map([ ['withDefaults', { name: 'withDefaults', kind: 'function', signature: 'export const withDefaults = (name: string, greeting: string = "Hello") => ...', parameters: ['name: string', 'greeting: string = "Hello"'] }] ]) }; const result = compareApiSurfaces(baseSurface, currentSurface, { name: 'test-pkg', path: '/test' }); // Default parameters should be treated as optional, so this is non-breaking expect(result.breakingChanges.length).toBe(0); expect(result.nonBreakingChanges.length).toBe(1); expect(result.nonBreakingChanges[0].type).toBe('parameter-added'); }); }); });