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:
parent
51b9ce44ea
commit
67c914819a
|
@ -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, ['&', '>', '<', 'ι']);
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue