diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts
index 3a5e237020..038a72d910 100644
--- a/packages/language-service/test/completions_spec.ts
+++ b/packages/language-service/test/completions_spec.ts
@@ -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: '
{{person.~{name}name}}
{
+ 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: '{{person.~{name}name}}
- }`);
- 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: '{{person.~{name}name}}
'})
- export class MyComponent {
- people: Promise;
- }`);
- 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, ['&', '>', '<', 'ι']);
});
- 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: \`
-
-
- \`,
- })
- 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));
}
diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts
index d967243b9b..958aea6bcd 100644
--- a/packages/language-service/test/project/app/parsing-cases.ts
+++ b/packages/language-service/test/project/app/parsing-cases.ts
@@ -143,10 +143,18 @@ export class ForUsingComponent {
{{person.~{async-person-name}name}}
+
+ {{person.~{promised-person-name}name}}
+
`,
})
export class AsyncForUsingComponent {
people: Promise = Promise.resolve([]);
+ promisedPerson: Promise = Promise.resolve({
+ name: 'John Doe',
+ age: 42,
+ street: '123 Angular Ln',
+ });
}
@Component({
diff --git a/packages/language-service/test/test_utils.ts b/packages/language-service/test/test_utils.ts
index 82769bd195..30637b7bbc 100644
--- a/packages/language-service/test/test_utils.ts
+++ b/packages/language-service/test/test_utils.ts
@@ -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;
diff --git a/packages/language-service/test/ts_plugin_spec.ts b/packages/language-service/test/ts_plugin_spec.ts
index fbe2063a8f..6cadb91830 100644
--- a/packages/language-service/test/ts_plugin_spec.ts
+++ b/packages/language-service/test/ts_plugin_spec.ts
@@ -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', '&', '>', '<', 'ι'); });
-
- 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 !, ...['&', '>', '<', 'ι']);
- });
-
- 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);
-}
+ });
+});