test(language-service): Move completions test to completions_spec.ts (#33159)

There are many specs in `ts_plugin_spec.ts` that exercise the behavior
of completions. These specs should belong in `completions_spec` instead.

In addition,

1. Tests for `getExternalFiles()` added in `ts_plugin_spec.ts`
2. Fixed bug in MockHost.reset() to remove overriden script names
3. Add test for TS diagnostics when `angularOnly = true` is not set

PR Close #33159
This commit is contained in:
Keen Yee Liau 2019-10-14 15:24:28 -07:00 committed by Matias Niemelä
parent 51b9ce44ea
commit 67c914819a
4 changed files with 464 additions and 348 deletions

View File

@ -9,172 +9,335 @@
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {CompletionKind} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {MockTypescriptHost} from './test_utils';
const APP_COMPONENT = '/app/app.component.ts';
const PARSING_CASES = '/app/parsing-cases.ts';
const TEST_TEMPLATE = '/app/test.ng';
const EXPRESSION_CASES = '/app/expression-cases.ts';
describe('completions', () => {
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
let service = ts.createLanguageService(mockHost);
let ngHost = new TypeScriptServiceHost(mockHost, service);
let ngService = createLanguageService(ngHost);
const mockHost = new MockTypescriptHost(['/app/main.ts']);
const tsLS = ts.createLanguageService(mockHost);
const ngHost = new TypeScriptServiceHost(mockHost, tsLS);
const ngLS = createLanguageService(ngHost);
beforeEach(() => { mockHost.reset(); });
it('should be able to get entity completions',
() => { expectContains('/app/test.ng', 'entity-amp', '&', '>', '<', 'ι'); });
it('should be able to get entity completions', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'entity-amp');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ENTITY, ['&', '>', '<', 'ι']);
});
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
expectContains('/app/test.ng', location, ...htmlTags);
const locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (const location of locations) {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, location);
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['div', 'h1', 'h2', 'span']);
}
});
it('should be able to return element diretives',
() => { expectContains('/app/test.ng', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { expectContains('/app/test.ng', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes',
() => { expectContains('/app/test.ng', 'div-attributes', '(click)', '[ngClass]'); });
it('should be able to infer the type of a ngForOf', () => {
const fileName = mockHost.addCode(`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people">{{person.~{name}name}}</div'})
export class MyComponent {
people: Person[]
}`);
expectContains(fileName, 'name', 'name', 'street');
it('should be able to return element directives', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'empty');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.COMPONENT, [
'ng-form',
'my-app',
'ng-component',
'test-comp',
]);
});
it('should be able to get completions for exported *ngIf variable', () => {
const fileName = mockHost.addCode(`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngIf="promised_person | async as person">{{person.~{name}name}}</div'})
export class MyComponent {
promised_person: Promise<Person>
}`);
expectContains(fileName, 'name', 'name', 'street');
it('should be able to return h1 attributes', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'h1-after-space');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, [
'class',
'id',
'onclick',
'onmouseup',
]);
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
const fileName = mockHost.addCode(`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people | async">{{person.~{name}name}}</div>'})
export class MyComponent {
people: Promise<Person[]>;
}`);
expectContains(fileName, 'name', 'name', 'street');
it('should be able to find common angular attributes', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'div-attributes');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, [
'(click)',
'[ngClass]',
'*ngIf',
'*ngFor',
]);
});
describe('with regression tests', () => {
it('should not crash with an incomplete component', () => {
expect(() => {
const fileName = mockHost.addCode(`
@Component({
template: '~{inside-template}'
})
export class MyComponent {
it('should be able to get the completions at the beginning of an interpolation', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'h2-hero');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'hero']);
});
}`);
it('should not include private members of a class', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'h2-hero');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expect(completions).toBeDefined();
const internal = completions !.entries.find(e => e.name === 'internal');
expect(internal).toBeUndefined();
});
expectContains(fileName, 'inside-template', 'h1');
}).not.toThrow();
it('should be able to get the completions at the end of an interpolation', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'sub-end');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'hero']);
});
it('should be able to get the completions in a property', () => {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'h2-name');
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should be able to return attribute names with an incompete attribute', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'no-value-attribute');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, ['id', 'class', 'dir', 'lang']);
});
it('should be able to return attributes of an incomplete element', () => {
const m1 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-lt');
const c1 = ngLS.getCompletionsAt(PARSING_CASES, m1.start);
expectContain(c1, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span']);
const m2 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-a');
const c2 = ngLS.getCompletionsAt(PARSING_CASES, m2.start);
expectContain(c2, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span']);
const m3 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-attr');
const c3 = ngLS.getCompletionsAt(PARSING_CASES, m3.start);
expectContain(c3, CompletionKind.HTML_ATTRIBUTE, ['id', 'class', 'href', 'name']);
});
it('should be able to return completions with a missing closing tag', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'missing-closing');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span', 'h1', 'h2']);
});
it('should be able to return common attributes of an unknown tag', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'unknown-element');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, ['id', 'dir', 'lang']);
});
it('should be able to get completions in an empty interpolation', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'empty-interpolation');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'subTitle']);
});
describe('in external template', () => {
it('should be able to get entity completions in external template', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'entity-amp');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ENTITY, ['&amp;', '&gt;', '&lt;', '&iota;']);
});
it('should hot crash with an incomplete class', () => {
expect(() => {
mockHost.addCode('\nexport class');
ngHost.getAnalyzedModules();
}).not.toThrow();
it('should be able to return html elements', () => {
const locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (const location of locations) {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, location);
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['div', 'h1', 'h2', 'span']);
}
});
});
it('should respect paths configuration', () => {
mockHost.overrideOptions({
baseUrl: '/app',
paths: {'bar/*': ['foo/bar/*']},
it('should be able to return element diretives', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'empty');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.COMPONENT, [
'ng-form',
'my-app',
'ng-component',
'test-comp',
]);
});
mockHost.addScript('/app/foo/bar/shared.ts', `
export interface Node {
children: Node[];
}
`);
mockHost.addScript('/app/my.component.ts', `
import { Component } from '@angular/core';
import { Node } from 'bar/shared';
@Component({
selector: 'my-component',
template: '{{tree.~{tree} }}'
})
export class MyComponent {
tree: Node;
}
`);
ngHost.getAnalyzedModules();
expectContains('/app/my.component.ts', 'tree', 'children');
it('should be able to return h1 attributes', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'h1-after-space');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, [
'class',
'id',
'onclick',
'onmouseup',
]);
});
it('should be able to find common angular attributes', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'div-attributes');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, [
'(click)',
'[ngClass]',
'*ngIf',
'*ngFor',
]);
});
});
it('should work with input and output', () => {
const fileName = mockHost.addCode(`
@Component({
selector: 'foo-component',
template: \`
<div string-model ~{stringMarker}="text"></div>
<div number-model ~{numberMarker}="value"></div>
\`,
})
export class FooComponent {
text: string;
value: number;
}
`);
expectContains(fileName, 'stringMarker', '[model]', '(model)');
expectContains(fileName, 'numberMarker', '[inputAlias]', '(outputAlias)');
describe('with a *ngIf', () => {
it('should be able to get completions for exported *ngIf variable', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'promised-person-name');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
});
});
function expectContains(fileName: string, locationMarker: string, ...names: string[]) {
const marker = mockHost.getLocationMarkerFor(fileName, locationMarker);
expectEntries(locationMarker, ngService.getCompletionsAt(fileName, marker.start), ...names);
}
describe('with a *ngFor', () => {
it('should include a let for empty attribute', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-empty');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.KEY, ['let', 'of']);
});
it('should suggest NgForRow members for let initialization expression', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-let-i-equal');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, [
'$implicit',
'ngForOf',
'index',
'count',
'first',
'last',
'even',
'odd',
]);
});
it('should include a let', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-let');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.KEY, ['let', 'of']);
});
it('should include an "of"', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-of');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.KEY, ['let', 'of']);
});
it('should include field reference', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-people');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['people']);
});
it('should include person in the let scope', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'for-interp-person');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.VARIABLE, ['person']);
});
it('should be able to infer the type of a ngForOf', () => {
for (const location of ['for-interp-name', 'for-interp-age']) {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, location);
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
}
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'async-person-name');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
});
});
describe('data binding', () => {
it('should be able to complete property value', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'property-binding-model');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['test']);
});
it('should be able to complete an event', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'event-binding-model');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.METHOD, ['modelChanged']);
});
it('should be able to complete a two-way binding', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'two-way-binding-model');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['test']);
});
it('should work with input and output', () => {
const m1 = mockHost.getLocationMarkerFor(PARSING_CASES, 'string-marker');
const c1 = ngLS.getCompletionsAt(PARSING_CASES, m1.start);
expectContain(c1, CompletionKind.ATTRIBUTE, ['[model]', '(model)']);
const m2 = mockHost.getLocationMarkerFor(PARSING_CASES, 'number-marker');
const c2 = ngLS.getCompletionsAt(PARSING_CASES, m2.start);
expectContain(c2, CompletionKind.ATTRIBUTE, ['[inputAlias]', '(outputAlias)']);
});
});
describe('for pipes', () => {
it('should be able to get a list of pipe values', () => {
for (const location of ['before-pipe', 'in-pipe', 'after-pipe']) {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, location);
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PIPE, [
'async',
'uppercase',
'lowercase',
'titlecase',
]);
}
});
it('should be able to resolve lowercase', () => {
const marker = mockHost.getLocationMarkerFor(EXPRESSION_CASES, 'string-pipe');
const completions = ngLS.getCompletionsAt(EXPRESSION_CASES, marker.start);
expectContain(completions, CompletionKind.METHOD, [
'charAt',
'replace',
'substring',
'toLowerCase',
]);
});
});
describe('with references', () => {
it('should list references', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-content');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.REFERENCE, ['div', 'test1', 'test2']);
});
it('should reference the component', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-after-test');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'testEvent']);
});
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should reference the element if no component', () => {
// const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'test-comp-after-div');
// const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
// expectContain(completions, CompletionKind.PROPERTY, ['innerText']);
// });
});
});
function expectEntries(
locationMarker: string, completion: ts.CompletionInfo | undefined, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!completion) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
}
if (!completion.entries.length) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`);
}
for (const entry of completion.entries) {
entries[entry.name] = true;
}
let missing = names.filter(name => !entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completion.entries.map(entry => entry.name).join(', ')}`);
}
function expectContain(
completions: ts.CompletionInfo | undefined, kind: CompletionKind, names: string[]) {
expect(completions).toBeDefined();
expect(completions !.entries).toEqual(jasmine.arrayContaining(names.map(name => {
return jasmine.objectContaining({name, kind});
}) as any));
}

View File

@ -143,10 +143,18 @@ export class ForUsingComponent {
<div *ngFor="let person of people | async">
{{person.~{async-person-name}name}}
</div>
<div *ngIf="promisedPerson | async as person">
{{person.~{promised-person-name}name}}
</div>
`,
})
export class AsyncForUsingComponent {
people: Promise<Person[]> = Promise.resolve([]);
promisedPerson: Promise<Person> = Promise.resolve({
name: 'John Doe',
age: 42,
street: '123 Angular Ln',
});
}
@Component({

View File

@ -185,6 +185,15 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
* Reset the project to its original state, effectively removing all overrides.
*/
reset() {
// Remove overrides from scriptNames
let length = 0;
for (let i = 0; i < this.scriptNames.length; ++i) {
const fileName = this.scriptNames[i];
if (!this.overrides.has(fileName)) {
this.scriptNames[length++] = fileName;
}
}
this.scriptNames.splice(length);
this.overrides.clear();
this.overrideDirectory.clear();
this.options = COMPILER_OPTIONS;

View File

@ -6,230 +6,166 @@
* found in the LICENSE file at https://angular.io/license
*/
import 'reflect-metadata';
import * as ts from 'typescript';
import {create} from '../src/ts_plugin';
import {create, getExternalFiles} from '../src/ts_plugin';
import {CompletionKind} from '../src/types';
import {MockTypescriptHost} from './test_utils';
describe('plugin', () => {
const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
const service = ts.createLanguageService(mockHost);
const program = service.getProgram();
const plugin = createPlugin(service, mockHost);
const mockProject = {
projectService: {
logger: {
info() {},
hasLevel: () => false,
},
},
hasRoots: () => true,
} as any;
it('should not report errors on tour of heroes', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (let source of program !.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(source.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(source.fileName));
describe('plugin', () => {
const mockHost = new MockTypescriptHost(['/app/main.ts']);
const tsLS = ts.createLanguageService(mockHost);
const program = tsLS.getProgram() !;
const plugin = create({
languageService: tsLS,
languageServiceHost: mockHost,
project: mockProject,
serverHost: {} as any,
config: {},
});
beforeEach(() => { mockHost.reset(); });
it('should produce TypeScript diagnostics', () => {
const fileName = '/foo.ts';
mockHost.addScript(fileName, `
function add(x: number) {
return x + 42;
}
add('hello');
`);
const diags = plugin.getSemanticDiagnostics(fileName);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Argument of type '"hello"' is not assignable to parameter of type 'number'.`);
});
it('should not report TypeScript errors on tour of heroes', () => {
const compilerDiags = tsLS.getCompilerOptionsDiagnostics();
expect(compilerDiags).toEqual([]);
const sourceFiles = program.getSourceFiles().filter(f => !f.fileName.endsWith('.d.ts'));
// there are six .ts files in the test project
expect(sourceFiles.length).toBe(6);
for (const {fileName} of sourceFiles) {
const syntacticDiags = tsLS.getSyntacticDiagnostics(fileName);
expect(syntacticDiags).toEqual([]);
const semanticDiags = tsLS.getSemanticDiagnostics(fileName);
expect(semanticDiags).toEqual([]);
}
});
it('should return external templates as external files', () => {
const externalFiles = getExternalFiles(mockProject);
expect(externalFiles).toEqual(['/app/test.ng']);
});
it('should not report template errors on tour of heroes', () => {
for (let source of program !.getSourceFiles()) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!source.fileName.endsWith('cases.ts')) {
expectNoDiagnostics(plugin.getSemanticDiagnostics(source.fileName));
const filesWithTemplates = [
// Ignore all '*-cases.ts' files as they intentionally contain errors.
'/app/app.component.ts',
];
for (const fileName of filesWithTemplates) {
const diags = plugin.getSemanticDiagnostics(fileName);
expect(diags).toEqual([]);
}
});
it('should respect paths configuration', () => {
const SHARED_MODULE = '/app/foo/bar/shared.ts';
const MY_COMPONENT = '/app/my.component.ts';
mockHost.overrideOptions({
baseUrl: '/app',
paths: {'bar/*': ['foo/bar/*']},
});
mockHost.addScript(SHARED_MODULE, `
export interface Node {
children: Node[];
}
`);
mockHost.addScript(MY_COMPONENT, `
import { Component, NgModule } from '@angular/core';
import { Node } from 'bar/shared';
@Component({
selector: 'my-component',
template: '{{ tree.~{tree}children }}'
})
export class MyComponent {
tree: Node = {
children: [],
};
}
@NgModule({
declarations: [MyComponent],
})
export class MyModule {}
`);
// First, make sure there are no errors in newly added scripts.
for (const fileName of [SHARED_MODULE, MY_COMPONENT]) {
const syntacticDiags = plugin.getSyntacticDiagnostics(fileName);
expect(syntacticDiags).toEqual([]);
const semanticDiags = plugin.getSemanticDiagnostics(fileName);
expect(semanticDiags).toEqual([]);
}
const marker = mockHost.getLocationMarkerFor(MY_COMPONENT, 'tree');
const completions = plugin.getCompletionsAtPosition(MY_COMPONENT, marker.start, undefined);
expect(completions).toBeDefined();
expect(completions !.entries).toEqual([
{
name: 'children',
kind: CompletionKind.PROPERTY as any,
sortText: 'children',
},
]);
});
it('should be able to get entity completions',
() => { contains('/app/app.component.ts', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('/app/app.component.ts', location, ...htmlTags);
}
});
it('should be able to return element directives',
() => { contains('/app/app.component.ts', 'empty', 'my-app'); });
it('should be able to return h1 attributes', () => {
contains('/app/app.component.ts', 'h1-after-space', 'id', 'dir', 'lang', 'onclick');
});
it('should be able to find common angular attributes', () => {
contains('/app/app.component.ts', 'div-attributes', '(click)', '[ngClass]', '*ngIf', '*ngFor');
});
it('should be able to return attribute names with an incompete attribute',
() => { contains('/app/parsing-cases.ts', 'no-value-attribute', 'id', 'dir', 'lang'); });
it('should be able to return attributes of an incomplete element', () => {
contains('/app/parsing-cases.ts', 'incomplete-open-lt', 'a');
contains('/app/parsing-cases.ts', 'incomplete-open-a', 'a');
contains('/app/parsing-cases.ts', 'incomplete-open-attr', 'id', 'dir', 'lang');
});
it('should be able to return completions with a missing closing tag',
() => { contains('/app/parsing-cases.ts', 'missing-closing', 'h1', 'h2'); });
it('should be able to return common attributes of an unknown tag',
() => { contains('/app/parsing-cases.ts', 'unknown-element', 'id', 'dir', 'lang'); });
it('should be able to get the completions at the beginning of an interpolation',
() => { contains('/app/app.component.ts', 'h2-hero', 'hero', 'title'); });
it('should not include private members of a class',
() => { contains('/app/app.component.ts', 'h2-hero', '-internal'); });
it('should be able to get the completions at the end of an interpolation',
() => { contains('/app/app.component.ts', 'sub-end', 'hero', 'title'); });
it('should be able to get the completions in a property',
() => { contains('/app/app.component.ts', 'h2-name', 'name', 'id'); });
it('should be able to get a list of pipe values', () => {
contains('/app/parsing-cases.ts', 'before-pipe', 'lowercase', 'uppercase');
contains('/app/parsing-cases.ts', 'in-pipe', 'lowercase', 'uppercase');
contains('/app/parsing-cases.ts', 'after-pipe', 'lowercase', 'uppercase');
});
it('should be able to get completions in an empty interpolation',
() => { contains('/app/parsing-cases.ts', 'empty-interpolation', 'title', 'subTitle'); });
describe('with attributes', () => {
it('should be able to complete property value',
() => { contains('/app/parsing-cases.ts', 'property-binding-model', 'test'); });
it('should be able to complete an event',
() => { contains('/app/parsing-cases.ts', 'event-binding-model', 'modelChanged'); });
it('should be able to complete a two-way binding',
() => { contains('/app/parsing-cases.ts', 'two-way-binding-model', 'test'); });
});
describe('with a *ngFor', () => {
it('should include a let for empty attribute',
() => { contains('/app/parsing-cases.ts', 'for-empty', 'let'); });
it('should suggest NgForRow members for let initialization expression', () => {
contains(
'/app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
'odd');
});
it('should include a let', () => { contains('/app/parsing-cases.ts', 'for-let', 'let'); });
it('should include an "of"', () => { contains('/app/parsing-cases.ts', 'for-of', 'of'); });
it('should include field reference',
() => { contains('/app/parsing-cases.ts', 'for-people', 'people'); });
it('should include person in the let scope',
() => { contains('/app/parsing-cases.ts', 'for-interp-person', 'person'); });
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
// contains('/app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('/app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
// });
});
describe('for pipes', () => {
it('should be able to resolve lowercase',
() => { contains('/app/expression-cases.ts', 'string-pipe', 'substring'); });
});
describe('with references', () => {
it('should list references',
() => { contains('/app/parsing-cases.ts', 'test-comp-content', 'test1', 'test2', 'div'); });
it('should reference the component',
() => { contains('/app/parsing-cases.ts', 'test-comp-after-test', 'name'); });
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should reference the element if no component', () => {
// contains('/app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
// });
});
describe('for semantic errors', () => {
describe(`with config 'angularOnly = true`, () => {
const ngLS = createPlugin(service, mockHost, {angularOnly: true});
it('should not report template errors on TOH', () => {
const sourceFiles = ngLS.getProgram() !.getSourceFiles();
expect(sourceFiles.length).toBeGreaterThan(0);
for (const {fileName} of sourceFiles) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!fileName.endsWith('cases.ts')) {
expectNoDiagnostics(ngLS.getSemanticDiagnostics(fileName));
}
}
});
it('should be able to get entity completions', () => {
const fileName = '/app/app.component.ts';
const marker = mockHost.getLocationMarkerFor(fileName, 'entity-amp');
const results = ngLS.getCompletionsAtPosition(fileName, marker.start, {} /* options */);
expect(results).toBeTruthy();
expectEntries('entity-amp', results !, ...['&amp;', '&gt;', '&lt;', '&iota;']);
});
it('should report template diagnostics', () => {
// TODO(kyliau): Rename these to end with '-error.ts'
const fileName = '/app/expression-cases.ts';
const diagnostics = ngLS.getSemanticDiagnostics(fileName);
expect(diagnostics.map(d => d.messageText)).toEqual([
`Identifier 'foo' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`,
`Identifier 'nam' is not defined. 'Person' does not contain such a member`,
`Identifier 'myField' refers to a private member of the component`,
`Expected a numeric type`,
]);
});
});
});
function createPlugin(tsLS: ts.LanguageService, tsLSHost: ts.LanguageServiceHost, config = {}) {
const project = {projectService: {logger: {info() {}}}};
return create({
languageService: tsLS,
languageServiceHost: tsLSHost,
project: project as any,
serverHost: {} as any,
config: {...config},
});
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
const marker = mockHost.getLocationMarkerFor(fileName, locationMarker);
expectEntries(
locationMarker, plugin.getCompletionsAtPosition(fileName, marker.start, undefined) !,
...names);
}
});
describe(`with config 'angularOnly = true`, () => {
const mockHost = new MockTypescriptHost(['/app/main.ts']);
const tsLS = ts.createLanguageService(mockHost);
const plugin = create({
languageService: tsLS,
languageServiceHost: mockHost,
project: mockProject,
serverHost: {} as any,
config: {
angularOnly: true,
},
});
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!info) {
throw new Error(`Expected result from ${locationMarker} to include ${names.join(
', ')} but no result provided`);
} else {
for (let entry of info.entries) {
entries[entry.name] = true;
}
let shouldContains = names.filter(name => !name.startsWith('-'));
let shouldNotContain = names.filter(name => name.startsWith('-'));
let missing = shouldContains.filter(name => !entries[name]);
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
if (missing.length) {
throw new Error(`Expected result from ${locationMarker
} to include at least one of the following, ${missing
.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name)
.join(', ')}`);
}
if (present.length) {
throw new Error(`Unexpected member${present.length > 1 ? 's' :
''
} included in result: ${present.join(', ')}`);
}
}
}
it('should not produce TypeScript diagnostics', () => {
const fileName = '/foo.ts';
mockHost.addScript(fileName, `
function add(x: number) {
return x + 42;
}
add('hello');
`);
const diags = plugin.getSemanticDiagnostics(fileName);
expect(diags).toEqual([]);
});
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
for (const diagnostic of diagnostics) {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.file && diagnostic.start) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.error(`${message}`);
it('should not report template errors on TOH', () => {
const filesWithTemplates = [
// Ignore all '*-cases.ts' files as they intentionally contain errors.
'/app/app.component.ts',
'/app/test.ng',
];
for (const fileName of filesWithTemplates) {
const diags = plugin.getSemanticDiagnostics(fileName);
expect(diags).toEqual([]);
}
}
expect(diagnostics.length).toBe(0);
}
});
});