test(language-service): Fix diagnostic tests (#32161)
This commit fixes many diagnostic tests have have incorrect/uncaught assertions. Also added more assertions to make sure TS diagnostics are clear. A few test util methods are removed to reduce clutter and improve readability. PR Close #32161
This commit is contained in:
parent
71ada483bf
commit
5f76de1d71
|
@ -7,393 +7,480 @@
|
|||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {createLanguageService} from '../src/language_service';
|
||||
import {Diagnostics, LanguageService} from '../src/types';
|
||||
import * as ng from '../src/types';
|
||||
import {TypeScriptServiceHost} from '../src/typescript_host';
|
||||
|
||||
import {toh} from './test_data';
|
||||
import {MockTypescriptHost, diagnosticMessageContains, findDiagnostic, includeDiagnostic, noDiagnostics} from './test_utils';
|
||||
import {MockTypescriptHost} from './test_utils';
|
||||
|
||||
/**
|
||||
* Note: If we want to test that a specific diagnostic message is emitted, then
|
||||
* use the `addCode()` helper method to add code to an existing file and check
|
||||
* that the diagnostic messages contain the expected output.
|
||||
*
|
||||
* If the goal is to assert that there is no error in a specific file, then use
|
||||
* `mockHost.override()` method to completely override an existing file, and
|
||||
* make sure no diagnostics are produced. When doing so, be extra cautious
|
||||
* about import statements and make sure to assert empty TS diagnostic messages
|
||||
* as well.
|
||||
*/
|
||||
|
||||
describe('diagnostics', () => {
|
||||
let mockHost: MockTypescriptHost;
|
||||
let ngHost: TypeScriptServiceHost;
|
||||
let ngService: LanguageService;
|
||||
let tsLS: ts.LanguageService;
|
||||
let ngLS: ng.LanguageService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
|
||||
const documentRegistry = ts.createDocumentRegistry();
|
||||
const service = ts.createLanguageService(mockHost, documentRegistry);
|
||||
ngHost = new TypeScriptServiceHost(mockHost, service);
|
||||
ngService = createLanguageService(ngHost);
|
||||
tsLS = ts.createLanguageService(mockHost);
|
||||
ngHost = new TypeScriptServiceHost(mockHost, tsLS);
|
||||
ngLS = createLanguageService(ngHost);
|
||||
});
|
||||
|
||||
it('should be no diagnostics for test.ng',
|
||||
() => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); });
|
||||
it('should produce no diagnostics for test.ng', () => {
|
||||
// there should not be any errors on existing external template
|
||||
expect(ngLS.getDiagnostics('/app/test.ng')).toEqual([]);
|
||||
});
|
||||
|
||||
describe('for semantic errors', () => {
|
||||
it('should not return TS and NG errors for existing files', () => {
|
||||
const files = [
|
||||
'/app/app.component.ts',
|
||||
'/app/main.ts',
|
||||
];
|
||||
for (const file of files) {
|
||||
const syntaxDiags = tsLS.getSyntacticDiagnostics(file);
|
||||
expect(syntaxDiags).toEqual([]);
|
||||
const semanticDiags = tsLS.getSemanticDiagnostics(file);
|
||||
expect(semanticDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(file);
|
||||
expect(ngDiags).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
// #17611
|
||||
it('should not report diagnostic on iteration of any', () => {
|
||||
const fileName = '/app/test.ng';
|
||||
|
||||
function diagnostics(template: string): ts.Diagnostic[] {
|
||||
try {
|
||||
mockHost.override(fileName, template);
|
||||
return ngService.getDiagnostics(fileName) !;
|
||||
} finally {
|
||||
mockHost.override(fileName, undefined !);
|
||||
}
|
||||
}
|
||||
|
||||
function accept(template: string) { noDiagnostics(diagnostics(template)); }
|
||||
|
||||
function reject(template: string, message: string): void;
|
||||
function reject(template: string, message: string, at: string): void;
|
||||
function reject(template: string, message: string, location: string): void;
|
||||
function reject(template: string, message: string, location: string, len: number): void;
|
||||
function reject(template: string, message: string, at?: number | string, len?: number): void {
|
||||
if (typeof at == 'string') {
|
||||
len = at.length;
|
||||
at = template.indexOf(at);
|
||||
}
|
||||
includeDiagnostic(diagnostics(template), message, at, len);
|
||||
}
|
||||
|
||||
describe('regression', () => {
|
||||
it('should be able to return diagnostics if reflector gets invalidated', () => {
|
||||
const fileName = '/app/main.ts';
|
||||
ngService.getDiagnostics(fileName);
|
||||
(ngHost as any)._reflector = null;
|
||||
ngService.getDiagnostics(fileName);
|
||||
});
|
||||
|
||||
// #17611
|
||||
it('should not report diagnostic on iteration of any',
|
||||
() => { accept('<div *ngFor="let value of anyValue">{{value.someField}}</div>'); });
|
||||
});
|
||||
|
||||
describe('with $event', () => {
|
||||
it('should accept an event',
|
||||
() => { accept('<div (click)="myClick($event)">Click me!</div>'); });
|
||||
it('should reject it when not in an event binding', () => {
|
||||
reject('<div [tabIndex]="$event"></div>', '\'$event\' is not defined', '$event');
|
||||
});
|
||||
});
|
||||
mockHost.override(fileName, '<div *ngFor="let value of anyValue">{{value.someField}}</div>');
|
||||
const diagnostics = ngLS.getDiagnostics(fileName);
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
describe('with regression tests', () => {
|
||||
|
||||
it('should not crash with a incomplete *ngFor', () => {
|
||||
expect(() => {
|
||||
const code =
|
||||
'\n@Component({template: \'<div *ngFor></div> ~{after-div}\'}) export class MyComponent {}';
|
||||
addCode(code, fileName => { ngService.getDiagnostics(fileName); });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should report a component not in a module', () => {
|
||||
const code = '\n@Component({template: \'<div></div>\'}) export class MyComponent {}';
|
||||
addCode(code, (fileName, content) => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
const offset = content !.lastIndexOf('@Component') + 1;
|
||||
const len = 'Component'.length;
|
||||
includeDiagnostic(
|
||||
diagnostics !, 'Component \'MyComponent\' is not included in a module', offset, len);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not report an error for a form\'s host directives', () => {
|
||||
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw getting diagnostics for an index expression', () => {
|
||||
const code =
|
||||
` @Component({template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'}) export class MyComponent {}`;
|
||||
addCode(
|
||||
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
|
||||
});
|
||||
|
||||
it('should not throw using a directive with no value', () => {
|
||||
const code =
|
||||
` @Component({template: '<form><input [(ngModel)]="name" required /></form>'}) export class MyComponent { name = 'some name'; }`;
|
||||
addCode(
|
||||
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
|
||||
});
|
||||
|
||||
it('should report an error for invalid metadata', () => {
|
||||
const code =
|
||||
` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`;
|
||||
addCode(code, (fileName, content) => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
includeDiagnostic(
|
||||
diagnostics !, 'Function expressions are not supported in decorators', '() => \'foo\'',
|
||||
content);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw for an invalid class', () => {
|
||||
const code = ` @Component({template: ''}) class`;
|
||||
addCode(
|
||||
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
|
||||
});
|
||||
|
||||
it('should not report an error for sub-types of string', () => {
|
||||
const code =
|
||||
` @Component({template: \`<div *ngIf="something === 'foo'"></div>\`}) export class MyComponent { something: 'foo' | 'bar'; }`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not report an error for sub-types of number', () => {
|
||||
const code =
|
||||
` @Component({template: \`<div *ngIf="something === 123"></div>\`}) export class MyComponent { something: 123 | 456; }`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report a warning if an event results in a callable expression', () => {
|
||||
const code =
|
||||
` @Component({template: \`<div (click)="onClick"></div>\`}) export class MyComponent { onClick() { } }`;
|
||||
addCode(code, (fileName, content) => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
includeDiagnostic(
|
||||
diagnostics !, 'Unexpected callable expression. Expected a method call', 'onClick',
|
||||
content);
|
||||
});
|
||||
});
|
||||
|
||||
// #13412
|
||||
it('should not report an error for using undefined', () => {
|
||||
const code =
|
||||
` @Component({template: \`<div *ngIf="something === undefined"></div>\`}) export class MyComponent { something = 'foo'; }})`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #13326
|
||||
it('should report a narrow span for invalid pipes', () => {
|
||||
const code =
|
||||
` @Component({template: '<p> Using an invalid pipe {{data | dat}} </p>'}) export class MyComponent { data = 'some data'; }`;
|
||||
addCode(code, fileName => {
|
||||
const diagnostic = findDiagnostic(ngService.getDiagnostics(fileName) !, 'pipe') !;
|
||||
expect(diagnostic).not.toBeUndefined();
|
||||
expect(diagnostic.length).toBeLessThan(11);
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #19406
|
||||
it('should allow empty template', () => {
|
||||
const appComponent = `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template : '',
|
||||
})
|
||||
export class AppComponent {}
|
||||
`;
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, appComponent);
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
describe('with $event', () => {
|
||||
it('should accept an event', () => {
|
||||
const fileName = '/app/test.ng';
|
||||
mockHost.override(fileName, '<div (click)="myClick($event)">Click me!</div>');
|
||||
const diagnostics = ngLS.getDiagnostics(fileName);
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
// Issue #15460
|
||||
it('should be able to find members defined on an ancestor type', () => {
|
||||
const app_component = `
|
||||
import { Component } from '@angular/core';
|
||||
import { NgForm } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: \`
|
||||
<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
|
||||
<input name="first" ngModel required #first="ngModel">
|
||||
<input name="last" ngModel>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
<p>First name value: {{ first.value }}</p>
|
||||
<p>First name valid: {{ first.valid }}</p>
|
||||
<p>Form value: {{ f.value | json }}</p>
|
||||
<p>Form valid: {{ f.valid }}</p>
|
||||
\`,
|
||||
})
|
||||
export class AppComponent {
|
||||
onSubmit(form: NgForm) {}
|
||||
}
|
||||
`;
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, app_component);
|
||||
const diagnostic = ngService.getDiagnostics(fileName);
|
||||
expect(diagnostic).toEqual([]);
|
||||
});
|
||||
|
||||
it('should report an error for invalid providers', () => {
|
||||
addCode(
|
||||
`
|
||||
@Component({
|
||||
template: '',
|
||||
providers: [null]
|
||||
})
|
||||
export class MyComponent {}
|
||||
`,
|
||||
fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName) !;
|
||||
const expected = findDiagnostic(diagnostics, 'Invalid providers for');
|
||||
const notExpected = findDiagnostic(diagnostics, 'Cannot read property');
|
||||
expect(expected).toBeDefined();
|
||||
expect(notExpected).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #15768
|
||||
it('should be able to parse a template reference', () => {
|
||||
addCode(
|
||||
`
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div *ngIf="comps | async; let comps; else loading">
|
||||
</div>
|
||||
<ng-template #loading>Loading comps...</ng-template>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
`,
|
||||
fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||
});
|
||||
|
||||
// Issue #15625
|
||||
it('should not report errors for localization syntax', () => {
|
||||
addCode(
|
||||
`
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div>
|
||||
{fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
|
||||
</div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {
|
||||
fieldCount: number;
|
||||
}
|
||||
`,
|
||||
fileName => {
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expectOnlyModuleDiagnostics(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
// Issue #15885
|
||||
it('should be able to remove null and undefined from a type', () => {
|
||||
mockHost.overrideOptions(options => {
|
||||
options.strictNullChecks = true;
|
||||
return options;
|
||||
});
|
||||
addCode(
|
||||
`
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \` {{test?.a}}
|
||||
\`
|
||||
})
|
||||
export class MyComponent {
|
||||
test: {a: number, b: number} | null = {
|
||||
a: 1,
|
||||
b: 2
|
||||
};
|
||||
}
|
||||
`,
|
||||
fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
|
||||
});
|
||||
|
||||
it('should be able to resolve modules using baseUrl', () => {
|
||||
const app_component = `
|
||||
import { Component } from '@angular/core';
|
||||
import { NgForm } from '@angular/common';
|
||||
import { Server } from 'app/server';
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: '...',
|
||||
providers: [Server]
|
||||
})
|
||||
export class AppComponent {
|
||||
onSubmit(form: NgForm) {}
|
||||
}
|
||||
`;
|
||||
const app_server = `
|
||||
export class Server {}
|
||||
`;
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, app_component);
|
||||
mockHost.addScript('/other/files/app/server.ts', app_server);
|
||||
mockHost.overrideOptions(options => {
|
||||
options.baseUrl = '/other/files';
|
||||
return options;
|
||||
});
|
||||
const diagnostic = ngService.getDiagnostics(fileName);
|
||||
expect(diagnostic).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not report errors for using the now removed OpaqueToken (support for v4)', () => {
|
||||
const app_component = `
|
||||
import { Component, Inject, OpaqueToken } from '@angular/core';
|
||||
import { NgForm } from '@angular/common';
|
||||
|
||||
export const token = new OpaqueToken();
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: '...'
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor (@Inject(token) value: string) {}
|
||||
onSubmit(form: NgForm) {}
|
||||
}
|
||||
`;
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, app_component);
|
||||
const diagnostics = ngService.getDiagnostics(fileName);
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const originalContent = mockHost.getFileContent(fileName);
|
||||
const newContent = originalContent + code;
|
||||
mockHost.override(fileName, originalContent + code);
|
||||
ngHost.getAnalyzedModules();
|
||||
try {
|
||||
cb(fileName, newContent);
|
||||
} finally {
|
||||
mockHost.override(fileName, undefined !);
|
||||
}
|
||||
}
|
||||
|
||||
function expectOnlyModuleDiagnostics(diagnostics: ts.Diagnostic[] | undefined) {
|
||||
// Expect only the 'MyComponent' diagnostic
|
||||
if (!diagnostics) throw new Error('Expecting Diagnostics');
|
||||
if (diagnostics.length > 1) {
|
||||
const unexpectedDiagnostics =
|
||||
diagnostics.filter(diag => !diagnosticMessageContains(diag.messageText, 'MyComponent'))
|
||||
.map(diag => `(${diag.start}:${diag.start! + diag.length!}): ${diag.messageText}`);
|
||||
|
||||
if (unexpectedDiagnostics.length) {
|
||||
fail(`Unexpected diagnostics:\n ${unexpectedDiagnostics.join('\n ')}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
it('should reject it when not in an event binding', () => {
|
||||
const fileName = '/app/test.ng';
|
||||
const content = mockHost.override(fileName, '<div [tabIndex]="$event"></div>');
|
||||
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
||||
expect(diagnostics.length).toBe(1);
|
||||
expect(diagnosticMessageContains(diagnostics[0].messageText, 'MyComponent')).toBeTruthy();
|
||||
}
|
||||
const {messageText, start, length} = diagnostics[0];
|
||||
expect(messageText)
|
||||
.toBe(
|
||||
'Identifier \'$event\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
|
||||
const keyword = '$event';
|
||||
expect(start).toBe(content.lastIndexOf(keyword));
|
||||
expect(length).toBe(keyword.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not crash with a incomplete *ngFor', () => {
|
||||
const fileName = addCode(`
|
||||
@Component({
|
||||
template: '<div *ngFor></div> ~{after-div}'
|
||||
})
|
||||
export class MyComponent {}`);
|
||||
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should report a component not in a module', () => {
|
||||
const fileName = addCode(`
|
||||
@Component({
|
||||
template: '<div></div>'
|
||||
})
|
||||
export class MyComponent {}`);
|
||||
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
||||
expect(diagnostics.length).toBe(1);
|
||||
const {messageText, start, length} = diagnostics[0];
|
||||
expect(messageText)
|
||||
.toBe(
|
||||
'Component \'MyComponent\' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.');
|
||||
const content = mockHost.getFileContent(fileName) !;
|
||||
const keyword = '@Component';
|
||||
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
|
||||
expect(length).toBe(keyword.length - 1); // exclude leading '@'
|
||||
});
|
||||
|
||||
|
||||
it(`should not report an error for a form's host directives`, () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<form></form>'})
|
||||
export class AppComponent {}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not throw getting diagnostics for an index expression', () => {
|
||||
const fileName = addCode(`
|
||||
@Component({
|
||||
template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'
|
||||
})
|
||||
export class MyComponent {}`);
|
||||
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw using a directive with no value', () => {
|
||||
const fileName = addCode(`
|
||||
@Component({
|
||||
template: '<form><input [(ngModel)]="name" required /></form>'
|
||||
})
|
||||
export class MyComponent {
|
||||
name = 'some name';
|
||||
}`);
|
||||
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should report an error for invalid metadata', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const content = mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<div></div>',
|
||||
providers: [
|
||||
{provide: 'foo', useFactory: () => 'foo' }
|
||||
]
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'some name';
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName) !;
|
||||
expect(ngDiags.length).toBe(1);
|
||||
const {messageText, start, length} = ngDiags[0];
|
||||
const keyword = `() => 'foo'`;
|
||||
expect(start).toBe(content.lastIndexOf(keyword));
|
||||
expect(length).toBe(keyword.length);
|
||||
// messageText is a three-part chain
|
||||
const firstPart = messageText as ts.DiagnosticMessageChain;
|
||||
expect(firstPart.messageText).toBe('Error during template compile of \'AppComponent\'');
|
||||
const secondPart = firstPart.next !;
|
||||
expect(secondPart.messageText).toBe('Function expressions are not supported in decorators');
|
||||
const thirdPart = secondPart.next !;
|
||||
expect(thirdPart.messageText)
|
||||
.toBe('Consider changing the function expression into an exported function');
|
||||
expect(thirdPart.next).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not throw for an invalid class', () => {
|
||||
const fileName = addCode(`
|
||||
@Component({
|
||||
template: ''
|
||||
}) class`);
|
||||
expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not report an error for sub-types of string', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: \`<div *ngIf="something === 'foo'"></div>\`
|
||||
})
|
||||
export class AppComponent {
|
||||
something: 'foo' | 'bar';
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not report an error for sub-types of number', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<div *ngIf="something === 123"></div>'
|
||||
})
|
||||
export class AppComponent {
|
||||
something: 123 | 456;
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should report a warning if an event results in a callable expression', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const content = mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<div (click)="onClick"></div>'
|
||||
})
|
||||
export class MyComponent {
|
||||
onClick() { }
|
||||
}`);
|
||||
const diagnostics = ngLS.getDiagnostics(fileName) !;
|
||||
const {messageText, start, length} = diagnostics[0];
|
||||
expect(messageText).toBe('Unexpected callable expression. Expected a method call');
|
||||
const keyword = `"onClick"`;
|
||||
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading quote
|
||||
expect(length).toBe(keyword.length - 2); // exclude leading and trailing quotes
|
||||
});
|
||||
|
||||
// #13412
|
||||
it('should not report an error for using undefined', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<div *ngIf="something === undefined"></div>'
|
||||
})
|
||||
export class AppComponent {
|
||||
something = 'foo';
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
// Issue #13326
|
||||
it('should report a narrow span for invalid pipes', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const content = mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '<p> Using an invalid pipe {{data | dat}} </p>'
|
||||
})
|
||||
export class AppComponent {
|
||||
data = 'some data';
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags.length).toBe(1);
|
||||
const {messageText, start, length} = ngDiags[0];
|
||||
expect(messageText).toBe(`The pipe 'dat' could not be found`);
|
||||
const keyword = 'data | dat';
|
||||
expect(start).toBe(content.lastIndexOf(keyword));
|
||||
expect(length).toBe(keyword.length);
|
||||
});
|
||||
|
||||
// Issue #19406
|
||||
it('should allow empty template', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template : '',
|
||||
})
|
||||
export class AppComponent {}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
// Issue #15460
|
||||
it('should be able to find members defined on an ancestor type', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: \`
|
||||
<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
|
||||
<input name="first" ngModel required #first="ngModel">
|
||||
<input name="last" ngModel>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
<p>First name value: {{ first.value }}</p>
|
||||
<p>First name valid: {{ first.valid }}</p>
|
||||
<p>Form value: {{ f.value | json }}</p>
|
||||
<p>Form valid: {{ f.valid }}</p>
|
||||
\`,
|
||||
})
|
||||
export class AppComponent {
|
||||
onSubmit(form: NgForm) {}
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should report an error for invalid providers', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const content = mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
providers: [null]
|
||||
})
|
||||
export class AppComponent {}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags.length).toBe(1);
|
||||
const {messageText, start, length} = ngDiags[0];
|
||||
expect(messageText)
|
||||
.toBe(
|
||||
'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]');
|
||||
// TODO: Looks like this is the wrong span. Should point to 'null' instead.
|
||||
const keyword = '@Component';
|
||||
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
|
||||
expect(length).toBe(keyword.length - 1); // exclude leading '@
|
||||
});
|
||||
|
||||
// Issue #15768
|
||||
it('should be able to parse a template reference', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div *ngIf="comps | async; let comps; else loading">
|
||||
</div>
|
||||
<ng-template #loading>Loading comps...</ng-template>
|
||||
\`
|
||||
})
|
||||
export class AppComponent {}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
// Issue #15625
|
||||
it('should not report errors for localization syntax', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div>
|
||||
{fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
|
||||
</div>
|
||||
\`
|
||||
})
|
||||
export class AppComponent {
|
||||
fieldCount: number;
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
// Issue #15885
|
||||
it('should be able to remove null and undefined from a type', () => {
|
||||
mockHost.overrideOptions(options => {
|
||||
options.strictNullChecks = true;
|
||||
return options;
|
||||
});
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: '{{test?.a}}',
|
||||
})
|
||||
export class AppComponent {
|
||||
test: {a: number, b: number} | null = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
};
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const ngDiags = ngLS.getDiagnostics(fileName);
|
||||
expect(ngDiags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should be able to resolve modules using baseUrl', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Server } from 'app/server';
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: '...',
|
||||
providers: [Server]
|
||||
})
|
||||
export class AppComponent {
|
||||
onSubmit(form: NgForm) {}
|
||||
}`);
|
||||
mockHost.addScript('/other/files/app/server.ts', 'export class Server {}');
|
||||
mockHost.overrideOptions(options => {
|
||||
options.baseUrl = '/other/files';
|
||||
return options;
|
||||
});
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags).toEqual([]);
|
||||
const diagnostic = ngLS.getDiagnostics(fileName);
|
||||
expect(diagnostic).toEqual([]);
|
||||
});
|
||||
|
||||
it('should report errors for using the now removed OpaqueToken (deprecated)', () => {
|
||||
const fileName = '/app/app.component.ts';
|
||||
mockHost.override(fileName, `
|
||||
import { Component, Inject, OpaqueToken } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
export const token = new OpaqueToken('some token');
|
||||
|
||||
@Component({
|
||||
selector: 'example-app',
|
||||
template: '...'
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor (@Inject(token) value: string) {}
|
||||
onSubmit(form: NgForm) {}
|
||||
}`);
|
||||
const tsDiags = tsLS.getSemanticDiagnostics(fileName);
|
||||
expect(tsDiags.length).toBe(1);
|
||||
expect(tsDiags[0].messageText)
|
||||
.toBe(
|
||||
`Module '"../node_modules/@angular/core/core"' has no exported member 'OpaqueToken'.`);
|
||||
});
|
||||
|
||||
function addCode(code: string) {
|
||||
const fileName = '/app/app.component.ts';
|
||||
const originalContent = mockHost.getFileContent(fileName);
|
||||
const newContent = originalContent + code;
|
||||
mockHost.override(fileName, newContent);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Diagnostic, DiagnosticMessageChain, Diagnostics, Span} from '../src/types';
|
||||
import {Span} from '../src/types';
|
||||
|
||||
export type MockData = string | MockDirectory;
|
||||
|
||||
|
@ -104,6 +104,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
|||
} else {
|
||||
this.overrides.delete(fileName);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
addScript(fileName: string, content: string) {
|
||||
|
@ -330,53 +331,3 @@ function getReferenceMarkers(value: string): ReferenceResult {
|
|||
function removeReferenceMarkers(value: string): string {
|
||||
return value.replace(referenceMarker, (match, text) => text.replace(/ᐱ/g, ''));
|
||||
}
|
||||
|
||||
export function noDiagnostics(diagnostics: ts.Diagnostic[]) {
|
||||
if (diagnostics && diagnostics.length) {
|
||||
throw new Error(
|
||||
`Unexpected diagnostics: \n ${diagnostics.map(d => d.messageText).join('\n ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function diagnosticMessageContains(
|
||||
message: string | ts.DiagnosticMessageChain, messageFragment: string): boolean {
|
||||
if (typeof message == 'string') {
|
||||
return message.indexOf(messageFragment) >= 0;
|
||||
}
|
||||
if (message.messageText.indexOf(messageFragment) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (message.next) {
|
||||
return diagnosticMessageContains(message.next, messageFragment);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findDiagnostic(
|
||||
diagnostics: ts.Diagnostic[], messageFragment: string): ts.Diagnostic|undefined {
|
||||
return diagnostics.find(d => diagnosticMessageContains(d.messageText, messageFragment));
|
||||
}
|
||||
|
||||
export function includeDiagnostic(
|
||||
diagnostics: ts.Diagnostic[], message: string, text?: string, len?: string): void;
|
||||
export function includeDiagnostic(
|
||||
diagnostics: ts.Diagnostic[], message: string, at?: number, len?: number): void;
|
||||
export function includeDiagnostic(
|
||||
diagnostics: ts.Diagnostic[], message: string, p1?: any, p2?: any) {
|
||||
expect(diagnostics).toBeDefined();
|
||||
if (diagnostics) {
|
||||
const diagnostic = findDiagnostic(diagnostics, message);
|
||||
expect(diagnostic).toBeDefined(`no diagnostic contains '${message}`);
|
||||
if (diagnostic && p1 != null) {
|
||||
const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
|
||||
const len = typeof p2 === 'number' ? p2 : p1.length;
|
||||
expect(diagnostic.start)
|
||||
.toEqual(
|
||||
at,
|
||||
`expected message '${message}' was reported at ${diagnostic.start} but should be ${at}`);
|
||||
if (len != null) {
|
||||
expect(diagnostic.length).toEqual(len, `expected '${message}'s span length to be ${len}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue