angular-cn/tools/ts-api-guardian/test/unit_test.ts

506 lines
13 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chai from 'chai';
import * as ts from 'typescript';
import {SerializationOptions, publicApiInternal} from '../lib/serializer';
const classesAndInterfaces = `
export declare class A {
field: string;
method(a: string): number;
}
export interface B {
field: A;
}
export declare class C {
someProp: string;
propWithDefault: number;
private privateProp;
protected protectedProp: number;
constructor(someProp: string, propWithDefault: number, privateProp: any, protectedProp: number);
}
`;
describe('unit test', () => {
let _warn: any = null;
let warnings: string[] = [];
beforeEach(() => {
_warn = console.warn;
console.warn = (...args: string[]) => warnings.push(args.join(' '));
});
afterEach(() => {
console.warn = _warn;
warnings = [];
_warn = null;
});
it('should ignore private methods', () => {
const input = `
export declare class A {
fa(): void;
protected fb(): void;
private fc();
}
`;
const expected = `
export declare class A {
fa(): void;
protected fb(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should support overloads functions', () => {
const input = `
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
export declare function registerLocaleData(data: any, extraData?: any): void;
export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
`;
const expected = `
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
export declare function registerLocaleData(data: any, extraData?: any): void;
export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
`;
check({'file.d.ts': input}, expected);
});
it('should ignore private props', () => {
const input = `
export declare class A {
fa: any;
protected fb: any;
private fc;
}
`;
const expected = `
export declare class A {
fa: any;
protected fb: any;
}
`;
check({'file.d.ts': input}, expected);
});
it('should support imports without capturing imports', () => {
const input = `
import {A} from './classes_and_interfaces';
export declare class C {
field: A;
}
`;
const expected = `
export declare class C {
field: A;
}
`;
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
});
it('should throw on aliased reexports', () => {
const input = `
export { A as Apple } from './classes_and_interfaces';
`;
checkThrows(
{'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input},
'Symbol "A" was aliased as "Apple". Aliases are not supported.');
});
it('should remove reexported external symbols', () => {
const input = `
export { Foo } from 'some-external-module-that-cannot-be-resolved';
`;
const expected = `
`;
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
chai.assert.deepEqual(
warnings, ['file.d.ts(1,1): error: No export declaration found for symbol "Foo"']);
});
it('should sort exports', () => {
const input = `
export declare type E = string;
export interface D {
e: number;
}
export declare var e: C;
export declare class C {
e: number;
d: string;
}
export declare function b(): boolean;
export declare const a: string;
`;
const expected = `
export declare const a: string;
export declare function b(): boolean;
export declare class C {
d: string;
e: number;
}
export interface D {
e: number;
}
export declare var e: C;
export declare type E = string;
`;
check({'file.d.ts': input}, expected);
});
it('should sort class members', () => {
const input = `
export class A {
f: number;
static foo(): void;
c: string;
static a: boolean;
constructor();
static bar(): void;
}
`;
const expected = `
export class A {
c: string;
f: number;
constructor();
static a: boolean;
static bar(): void;
static foo(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort interface members', () => {
const input = `
export interface A {
(): void;
[key: string]: any;
c(): void;
a: number;
new (): Object;
}
`;
const expected = `
export interface A {
a: number;
(): void;
new (): Object;
[key: string]: any;
c(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort class members including readonly', () => {
const input = `
export declare class DebugNode {
private _debugContext;
nativeNode: any;
listeners: any[];
parent: any | null;
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
readonly injector: any;
readonly componentInstance: any;
readonly context: any;
readonly references: {
[key: string]: any;
};
readonly providerTokens: any[];
}
`;
const expected = `
export declare class DebugNode {
readonly componentInstance: any;
readonly context: any;
readonly injector: any;
listeners: any[];
nativeNode: any;
parent: any | null;
readonly providerTokens: any[];
readonly references: {
[key: string]: any;
};
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort two call signatures', () => {
const input = `
export interface A {
(b: number): void;
(a: number): void;
}
`;
const expected = `
export interface A {
(a: number): void;
(b: number): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort exports including re-exports', () => {
const submodule = `
export declare var e: C;
export declare class C {
e: number;
d: string;
}
`;
const input = `
export * from './submodule';
export declare type E = string;
export interface D {
e: number;
}
export declare function b(): boolean;
export declare const a: string;
`;
const expected = `
export declare const a: string;
export declare function b(): boolean;
export declare class C {
d: string;
e: number;
}
export interface D {
e: number;
}
export declare var e: C;
export declare type E = string;
`;
check({'submodule.d.ts': submodule, 'file.d.ts': input}, expected);
});
it('should remove module comments', () => {
const input = `
/**
* An amazing module.
* @module
*/
/**
* Foo function.
*/
export declare function foo(): boolean;
export declare const bar: number;
`;
const expected = `
export declare const bar: number;
export declare function foo(): boolean;
`;
check({'file.d.ts': input}, expected);
});
it('should remove class and field comments', () => {
const input = `
/**
* Does something really cool.
*/
export declare class A {
/**
* A very interesting getter.
*/
b: string;
/**
* A very useful field.
*/
name: string;
}
`;
const expected = `
export declare class A {
b: string;
name: string;
}
`;
check({'file.d.ts': input}, expected);
});
it('should skip symbols matching specified pattern', () => {
const input = `
export const __a__: string;
export class B {
}
`;
const expected = `
export class B {
}
`;
check({'file.d.ts': input}, expected, {stripExportPattern: /^__.*/});
});
it('should throw on using non-whitelisted module imports in expression position', () => {
const input = `
import * as foo from './foo';
export declare class A extends foo.A {
}
`;
checkThrows(
{'file.d.ts': input}, 'file.d.ts(2,32): error: Module identifier "foo" is not allowed. ' +
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
});
it('should throw on using non-whitelisted module imports in type position', () => {
const input = `
import * as foo from './foo';
export type A = foo.A;
`;
checkThrows(
{'file.d.ts': input}, 'file.d.ts(2,17): error: Module identifier "foo" is not allowed. ' +
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
});
it('should not throw on using whitelisted module imports', () => {
const input = `
import * as foo from './foo';
export declare class A extends foo.A {
}
`;
const expected = `
export declare class A extends foo.A {
}
`;
check({'file.d.ts': input}, expected, {allowModuleIdentifiers: ['foo']});
});
it('should not throw if non-whitelisted module imports are not written', () => {
const input = `
import * as foo from './foo';
export declare class A {
}
`;
const expected = `
export declare class A {
}
`;
check({'file.d.ts': input}, expected);
});
it('should keep stability annotations of exports in docstrings', () => {
const input = `
/**
* @deprecated This is useless now
*/
export declare class A {
}
/**
* @experimental
*/
export declare const b: string;
/**
* @stable
*/
export declare var c: number;
`;
const expected = `
/** @deprecated */
export declare class A {
}
/** @experimental */
export declare const b: string;
/** @stable */
export declare var c: number;
`;
check({'file.d.ts': input}, expected);
});
it('should keep stability annotations of fields in docstrings', () => {
const input = `
export declare class A {
/**
* @stable
*/
value: number;
/**
* @experimental
*/
constructor();
/**
* @deprecated
*/
foo(): void;
}
`;
const expected = `
export declare class A {
/** @stable */ value: number;
/** @experimental */ constructor();
/** @deprecated */ foo(): void;
}
`;
check({'file.d.ts': input}, expected);
});
});
function getMockHost(files: {[name: string]: string}): ts.CompilerHost {
return {
getSourceFile: (sourceName, languageVersion) => {
if (!files[sourceName]) return undefined;
return ts.createSourceFile(
sourceName, stripExtraIndentation(files[sourceName]), languageVersion, true);
},
writeFile: (name, text, writeByteOrderMark) => {},
fileExists: (filename) => !!files[filename],
readFile: (filename) => stripExtraIndentation(files[filename]),
getDefaultLibFileName: () => 'lib.ts',
useCaseSensitiveFileNames: () => true,
getCanonicalFileName: (filename) => filename,
getCurrentDirectory: () => './',
getNewLine: () => '\n',
getDirectories: () => []
};
}
function check(
files: {[name: string]: string}, expected: string, options: SerializationOptions = {}) {
const actual = publicApiInternal(getMockHost(files), 'file.d.ts', {}, options);
chai.assert.equal(actual.trim(), stripExtraIndentation(expected).trim());
}
function checkThrows(files: {[name: string]: string}, error: string) {
chai.assert.throws(() => { publicApiInternal(getMockHost(files), 'file.d.ts', {}); }, error);
}
function stripExtraIndentation(text: string) {
let lines = text.split('\n');
// Ignore first and last new line
lines = lines.slice(1, lines.length - 1);
const commonIndent = lines.reduce((min, line) => {
const indent = /^( *)/.exec(line) ![1].length;
// Ignore empty line
return line.length ? Math.min(min, indent) : min;
}, text.length);
return lines.map(line => line.substr(commonIndent)).join('\n') + '\n';
}