/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as 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 TEST_TEMPLATE = '/app/test.ng';
describe('completions', () => {
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 return html elements', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '<~{cursor}');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.HTML_ELEMENT, ['div', 'h1', 'h2', 'span']);
});
it('should be able to return component directives', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '<~{cursor}');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.COMPONENT, [
'ng-form',
'my-app',
'ng-component',
'test-comp',
]);
});
it('should be able to return attribute directives', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, ['string-model', 'number-model']);
});
it('should be able to return angular pseudo elements', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, `<~{cursor}`);
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ANGULAR_ELEMENT, [
'ng-container',
'ng-content',
'ng-template',
]);
});
it('should be able to return h1 attributes', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, [
'class',
'id',
'onclick',
'onmouseup',
]);
});
it('should be able to find common Angular attributes', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, [
'ngClass',
'ngForm',
'ngModel',
'string-model',
'number-model',
]);
});
it('should be able to get the completions at the beginning of an interpolation', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
{{ ~{cursor} }}
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'hero']);
});
it('should not include private members of a class', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
{{ ~{cursor} }}
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expect(completions).toBeDefined();
const internal = completions!.entries.find(e => e.name === 'internal');
expect(internal).toBeUndefined();
});
it('should be able to get the completions at the end of an interpolation', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '{{ti~{cursor}}}');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'hero']);
});
it('should be able to get the completions in a property', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '
{{ hero.~{cursor} }}
');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'cursor');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should suggest template references', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, [
'ngFor',
'ngForOf',
'ngIf',
'ngSwitchCase',
'ngSwitchDefault',
'ngPluralCase',
'ngTemplateOutlet',
]);
});
it('should be able to return attribute names with an incomplete attribute', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, '');
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'no-value-attribute');
const completions = ngLS.getCompletionsAtPosition(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, ['id', 'class', 'dir', 'lang']);
});
it('should be able to return attributes of an incomplete element', () => {
mockHost.overrideInlineTemplate(APP_COMPONENT, `
Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text
');
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expect(completions).toBeDefined();
const {entries} = completions!;
expect(entries).not.toContain(jasmine.objectContaining({name: 'class'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'id'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'onclick'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'onmouseup'}));
});
it('should be able to find common Angular attributes', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, [
'ngClass',
'ngForm',
'ngModel',
'string-model',
'number-model',
]);
});
});
describe('with a *ngIf', () => {
it('should be able to get completions for exported *ngIf variable', () => {
mockHost.override(TEST_TEMPLATE, `
{{ h.~{cursor} }}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
});
describe('with a *ngFor', () => {
it('should suggest NgForRow members for let initialization expression', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, [
'$implicit',
'ngForOf',
'index',
'count',
'first',
'last',
'even',
'odd',
]);
});
it('should not provide suggestion before the = sign', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expect(completions).toBeUndefined();
});
describe('template binding: key expression', () => {
it('should complete the RHS of a template key expression without an expression value', () => {
mockHost.override(
TEST_TEMPLATE, ``); // value is undefined
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']);
// the symbol 'x' declared in *ngFor is also in scope. This asserts that
// we are actually taking the AST into account and not just referring to
// the symbol table of the Component.
expectContain(completions, CompletionKind.VARIABLE, ['x']);
});
it('should complete the RHS of a template key expression with an expression value', () => {
mockHost.override(
TEST_TEMPLATE, ``); // value is defined
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title', 'heroes', 'league']);
// the symbol 'x' declared in *ngFor is also in scope. This asserts that
// we are actually taking the AST into account and not just referring to
// the symbol table of the Component.
expectContain(completions, CompletionKind.VARIABLE, ['x']);
});
});
it('should include expression completions', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'expr-property-read');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name']);
});
it('should include variable in the let scope in interpolation', () => {
mockHost.override(TEST_TEMPLATE, `
{{~{cursor}}}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.VARIABLE, ['h']);
});
it('should be able to infer the type of a ngForOf', () => {
mockHost.override(TEST_TEMPLATE, `
{{ h.~{cursor} }}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
mockHost.override(TEST_TEMPLATE, `
{{ h.~{cursor} }}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should be able to resolve variable in nested loop', () => {
mockHost.override(TEST_TEMPLATE, `
{{member.~{position}}}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'position');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
// member variable of type Hero has properties 'id' and 'name'.
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
});
describe('data binding', () => {
it('should be able to complete property value', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title']);
});
it('should be able to complete property read', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'property-read');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
});
it('should be able to complete an event', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.METHOD, ['myClick']);
});
it('should be able to complete a the LHS of a two-way binding', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, ['ngModel']);
});
it('should be able to complete a the RHS of a two-way binding', () => {
mockHost.override(TEST_TEMPLATE, ``);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['title']);
});
it('should suggest property binding for input', () => {
// Property binding via []
mockHost.override(TEST_TEMPLATE, ``);
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c1 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m1.start);
expectContain(c1, CompletionKind.ATTRIBUTE, ['inputAlias']);
// Property binding via bind-
mockHost.override(TEST_TEMPLATE, ``);
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c2 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m2.start);
expectContain(c2, CompletionKind.ATTRIBUTE, ['inputAlias']);
});
it('should suggest event binding for output', () => {
// Event binding via ()
mockHost.override(TEST_TEMPLATE, ``);
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c1 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m1.start);
expectContain(c1, CompletionKind.ATTRIBUTE, ['outputAlias']);
// Event binding via on-
mockHost.override(TEST_TEMPLATE, ``);
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c2 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m2.start);
expectContain(c2, CompletionKind.ATTRIBUTE, ['outputAlias']);
});
it('should suggest two-way binding for input and output', () => {
// Banana-in-a-box via [()]
mockHost.override(TEST_TEMPLATE, ``);
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c1 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m1.start);
expectContain(c1, CompletionKind.ATTRIBUTE, ['model']);
// Banana-in-a-box via bindon-
mockHost.override(TEST_TEMPLATE, ``);
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const c2 = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, m2.start);
expectContain(c2, CompletionKind.ATTRIBUTE, ['model']);
});
});
describe('for pipes', () => {
it('should be able to get a list of pipe values', () => {
// TODO(kyliau): does not work for case {{ title | ~{cursor} }}
// space before and after pipe ^^^
mockHost.override(TEST_TEMPLATE, `{{ title|~{cursor} }}`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PIPE, [
'async',
'lowercase',
'slice',
'titlecase',
'uppercase',
]);
});
it('should be able to resolve lowercase', () => {
mockHost.override(TEST_TEMPLATE, `{{ (title | lowercase).~{cursor} }}`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.METHOD, [
'charAt',
'replace',
'substring',
'toLowerCase',
]);
});
});
describe('with references', () => {
it('should list references', () => {
mockHost.override(TEST_TEMPLATE, `
{{ ~{cursor} }}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.REFERENCE, ['myDiv', 'test1', 'test2']);
});
it('should reference the component', () => {
mockHost.override(TEST_TEMPLATE, `
{{ test1.~{cursor} }}
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'testEvent']);
});
it('should get reference property completions in a data binding', () => {
mockHost.override(TEST_TEMPLATE, `
`);
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'property-read');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, 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.getCompletionsAtPosition(PARSING_CASES, marker.start);
// expectContain(completions, CompletionKind.PROPERTY, ['innerText']);
// });
});
describe('replacement span', () => {
it('should not generate replacement entries for zero-length replacements', () => {
const fileName = mockHost.addCode(`
@Component({
selector: 'foo-component',
template: \`