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