2016-11-22 12:10:23 -05:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. 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';
|
2019-10-29 19:32:18 -04:00
|
|
|
import {CompletionKind} from '../src/types';
|
2016-11-22 12:10:23 -05:00
|
|
|
import {TypeScriptServiceHost} from '../src/typescript_host';
|
|
|
|
|
2017-01-03 20:21:45 -05:00
|
|
|
import {MockTypescriptHost} from './test_utils';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
describe('completions', () => {
|
|
|
|
const mockHost = new MockTypescriptHost(['/app/main.ts']);
|
|
|
|
const tsLS = ts.createLanguageService(mockHost);
|
|
|
|
const ngHost = new TypeScriptServiceHost(mockHost, tsLS);
|
|
|
|
const ngLS = createLanguageService(ngHost);
|
2019-09-17 17:33:41 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
beforeEach(() => { mockHost.reset(); });
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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, ['&', '>', '<', 'ι']);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
it('should be able to return html elements', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
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);
|
2019-10-24 17:48:43 -04:00
|
|
|
expectContain(completions, CompletionKind.HTML_ELEMENT, ['div', 'h1', 'h2', 'span']);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-10-21 21:49:32 -04:00
|
|
|
it('should be able to return component directives', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
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',
|
|
|
|
]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-21 21:49:32 -04:00
|
|
|
it('should be able to return attribute directives', () => {
|
|
|
|
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'h1-after-space');
|
|
|
|
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ATTRIBUTE, ['string-model', 'number-model']);
|
|
|
|
});
|
|
|
|
|
2019-09-19 02:13:30 -04:00
|
|
|
it('should be able to return angular pseudo elements', () => {
|
|
|
|
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'empty');
|
|
|
|
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ANGULAR_ELEMENT, [
|
|
|
|
'ng-container',
|
|
|
|
'ng-content',
|
|
|
|
'ng-template',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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',
|
|
|
|
]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-12-12 18:46:27 -05:00
|
|
|
it('should be able to find common Angular attributes', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'div-attributes');
|
|
|
|
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ATTRIBUTE, [
|
2019-12-12 18:46:27 -05:00
|
|
|
'ngClass',
|
|
|
|
'ngForm',
|
|
|
|
'ngModel',
|
|
|
|
'string-model',
|
|
|
|
'number-model',
|
2019-10-14 18:24:28 -04:00
|
|
|
]);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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();
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
});
|
2019-10-05 08:18:05 -04:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
2019-11-12 16:57:44 -05:00
|
|
|
});
|
|
|
|
|
2019-12-18 14:08:31 -05:00
|
|
|
it('should suggest template references', () => {
|
2019-12-12 18:46:27 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `<div *~{cursor}></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ATTRIBUTE, [
|
|
|
|
'ngFor',
|
|
|
|
'ngForOf',
|
|
|
|
'ngIf',
|
|
|
|
'ngSwitchCase',
|
|
|
|
'ngSwitchDefault',
|
|
|
|
'ngPluralCase',
|
|
|
|
]);
|
2019-10-05 08:18:05 -04:00
|
|
|
});
|
|
|
|
|
2019-12-12 18:46:27 -05:00
|
|
|
it('should be able to return attribute names with an incomplete attribute', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
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);
|
2019-10-24 17:48:43 -04:00
|
|
|
expectContain(c1, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span']);
|
2019-10-14 18:24:28 -04:00
|
|
|
|
|
|
|
const m2 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-a');
|
|
|
|
const c2 = ngLS.getCompletionsAt(PARSING_CASES, m2.start);
|
2019-10-24 17:48:43 -04:00
|
|
|
expectContain(c2, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span']);
|
2019-10-14 18:24:28 -04:00
|
|
|
|
|
|
|
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']);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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);
|
2019-10-24 17:48:43 -04:00
|
|
|
expectContain(completions, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span', 'h1', 'h2']);
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
});
|
|
|
|
|
2019-10-17 21:42:27 -04:00
|
|
|
it('should suggest $any() type cast function in an interpolation', () => {
|
|
|
|
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, 'sub-start');
|
|
|
|
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.METHOD, ['$any']);
|
|
|
|
});
|
|
|
|
|
2019-11-14 19:56:01 -05:00
|
|
|
it('should suggest attribute values', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div [id]="~{cursor}"></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, [
|
|
|
|
'title',
|
|
|
|
'hero',
|
|
|
|
'heroes',
|
|
|
|
'league',
|
|
|
|
'anyValue',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should suggest event handlers', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div (click)="~{cursor}"></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.METHOD, ['myClick']);
|
|
|
|
});
|
|
|
|
|
2019-11-15 15:06:00 -05:00
|
|
|
it('for methods should include parentheses', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div (click)="~{cursor}"></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
expect(completions !.entries).toContain(jasmine.objectContaining({
|
|
|
|
name: 'myClick',
|
|
|
|
kind: CompletionKind.METHOD as any,
|
|
|
|
insertText: 'myClick()',
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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, ['&', '>', '<', 'ι']);
|
|
|
|
});
|
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
it('should not return html elements', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
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);
|
2019-10-24 17:48:43 -04:00
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const {entries} = completions !;
|
|
|
|
expect(entries).not.toContain(jasmine.objectContaining({name: 'div'}));
|
|
|
|
expect(entries).not.toContain(jasmine.objectContaining({name: 'h1'}));
|
|
|
|
expect(entries).not.toContain(jasmine.objectContaining({name: 'h2'}));
|
|
|
|
expect(entries).not.toContain(jasmine.objectContaining({name: 'span'}));
|
2019-10-14 18:24:28 -04:00
|
|
|
}
|
|
|
|
});
|
2019-09-17 17:33:41 -04:00
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
it('should be able to return element directives', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
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',
|
|
|
|
]);
|
|
|
|
});
|
2019-09-12 22:34:05 -04:00
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
it('should not return html attributes', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'h1-after-space');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
2019-10-24 17:48:43 -04:00
|
|
|
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'}));
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2019-12-12 18:46:27 -05:00
|
|
|
it('should be able to find common Angular attributes', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div ~{cursor}></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
2019-10-14 18:24:28 -04:00
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ATTRIBUTE, [
|
2019-12-12 18:46:27 -05:00
|
|
|
'ngClass',
|
|
|
|
'ngForm',
|
|
|
|
'ngModel',
|
|
|
|
'string-model',
|
|
|
|
'number-model',
|
2019-10-14 18:24:28 -04:00
|
|
|
]);
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
describe('with a *ngFor', () => {
|
|
|
|
it('should suggest NgForRow members for let initialization expression', () => {
|
2019-12-18 14:08:31 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let i=~{cursor}"></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
2019-10-14 18:24:28 -04:00
|
|
|
expectContain(completions, CompletionKind.PROPERTY, [
|
|
|
|
'$implicit',
|
|
|
|
'ngForOf',
|
|
|
|
'index',
|
|
|
|
'count',
|
|
|
|
'first',
|
|
|
|
'last',
|
|
|
|
'even',
|
|
|
|
'odd',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2019-12-18 14:08:31 -05:00
|
|
|
it('should not provide suggestion before the = sign', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let i~{cursor}="></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expect(completions).toBeUndefined();
|
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
it('should include field reference', () => {
|
2019-12-18 14:08:31 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let x of ~{cursor}"></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(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']);
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
|
2019-12-18 14:08:31 -05:00
|
|
|
it('should include variable in the let scope in interpolation', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `
|
|
|
|
<div *ngFor="let h of heroes">
|
|
|
|
{{~{cursor}}}
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.VARIABLE, ['h']);
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to infer the type of a ngForOf', () => {
|
2019-12-18 14:08:31 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `
|
|
|
|
<div *ngFor="let h of heroes">
|
|
|
|
{{ h.~{cursor} }}
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
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']);
|
|
|
|
});
|
2019-11-06 13:32:45 -05:00
|
|
|
|
|
|
|
it('should be able to resolve variable in nested loop', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `
|
|
|
|
<div *ngFor="let leagueMembers of league">
|
|
|
|
<div *ngFor="let member of leagueMembers">
|
|
|
|
{{member.~{position}}}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'position');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
// member variable of type Hero has properties 'id' and 'name'.
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
|
|
|
});
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
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']);
|
|
|
|
});
|
|
|
|
|
2019-10-21 21:49:32 -04:00
|
|
|
it('should be able to complete a the LHS of a two-way binding', () => {
|
2019-12-12 18:46:27 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `<div [(~{cursor})]></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.ATTRIBUTE, ['ngModel']);
|
2019-10-21 21:49:32 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to complete a the RHS of a two-way binding', () => {
|
2019-10-14 18:24:28 -04:00
|
|
|
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'two-way-binding-model');
|
|
|
|
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['test']);
|
|
|
|
});
|
|
|
|
|
2019-12-12 18:46:27 -05:00
|
|
|
it('should suggest property binding for input', () => {
|
|
|
|
// Property binding via []
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div number-model [~{cursor}]></div>`);
|
|
|
|
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start);
|
|
|
|
expectContain(c1, CompletionKind.ATTRIBUTE, ['inputAlias']);
|
|
|
|
|
|
|
|
// Property binding via bind-
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div number-model bind-~{cursor}></div>`);
|
|
|
|
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c2 = ngLS.getCompletionsAt(TEST_TEMPLATE, m2.start);
|
|
|
|
expectContain(c2, CompletionKind.ATTRIBUTE, ['inputAlias']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should suggest event binding for output', () => {
|
|
|
|
// Event binding via ()
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div number-model (~{cursor})></div>`);
|
|
|
|
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start);
|
|
|
|
expectContain(c1, CompletionKind.ATTRIBUTE, ['outputAlias']);
|
|
|
|
|
|
|
|
// Event binding via on-
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div number-mode on-~{cursor}></div>`);
|
|
|
|
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c2 = ngLS.getCompletionsAt(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, `<div string-model [(~{cursor})]></div>`);
|
|
|
|
const m1 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c1 = ngLS.getCompletionsAt(TEST_TEMPLATE, m1.start);
|
|
|
|
expectContain(c1, CompletionKind.ATTRIBUTE, ['model']);
|
2019-10-14 18:24:28 -04:00
|
|
|
|
2019-12-12 18:46:27 -05:00
|
|
|
// Banana-in-a-box via bindon-
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div string-model bindon-~{cursor}></div>`);
|
|
|
|
const m2 = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const c2 = ngLS.getCompletionsAt(TEST_TEMPLATE, m2.start);
|
|
|
|
expectContain(c2, CompletionKind.ATTRIBUTE, ['model']);
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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',
|
|
|
|
]);
|
2019-01-30 13:34:36 -05:00
|
|
|
}
|
2019-10-14 18:24:28 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
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',
|
|
|
|
]);
|
|
|
|
});
|
2019-01-30 13:34:36 -05:00
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
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']);
|
|
|
|
// });
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
describe('replacement span', () => {
|
|
|
|
it('should not generate replacement entries for zero-length replacements', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
|
|
|
<div>{{obj.~{key}}}</div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {
|
|
|
|
obj: {key: 'value'};
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'key');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'key') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('property');
|
|
|
|
expect(completion.replacementSpan).toBeUndefined();
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for start of template', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`~{start}abc\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'start');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'acronym') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('html element');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start, length: 3});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for end of template', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`acro~{end}\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'end');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'acronym') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('html element');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 4, length: 4});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for middle-word replacements', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
|
|
|
<div>{{obj.ke~{key}key}}</div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {
|
|
|
|
obj: {key: 'value'};
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'key');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'key') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('property');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 2, length: 5});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for all kinds of identifier characters', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
|
|
|
<div>{{~{field}$title_1}}</div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {
|
|
|
|
$title_1: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'field');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === '$title_1') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('property');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start, length: 8});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for attributes', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
2019-12-12 18:46:27 -05:00
|
|
|
<div (cl~{click})></div>
|
2019-10-29 19:32:18 -04:00
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'click');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
2019-12-12 18:46:27 -05:00
|
|
|
const completion = completions.entries.find(entry => entry.name === 'click') !;
|
2019-10-29 19:32:18 -04:00
|
|
|
expect(completion).toBeDefined();
|
2019-12-12 18:46:27 -05:00
|
|
|
expect(completion.kind).toBe(CompletionKind.ATTRIBUTE);
|
2019-10-29 19:32:18 -04:00
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 2, length: 2});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for events', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
|
|
|
<div (click)="han~{handleClick}"></div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {
|
|
|
|
handleClick() {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'handleClick');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'handleClick') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('method');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 3, length: 3});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for element names', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
|
|
|
<di~{div}></div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'div');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
|
|
|
const completion = completions.entries.find(entry => entry.name === 'div') !;
|
|
|
|
expect(completion).toBeDefined();
|
|
|
|
expect(completion.kind).toBe('html element');
|
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 2, length: 2});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
|
2019-10-29 19:32:18 -04:00
|
|
|
it('should work for bindings', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
selector: 'foo-component',
|
|
|
|
template: \`
|
2019-12-12 18:46:27 -05:00
|
|
|
<input [(ngMod~{model})] />
|
2019-10-29 19:32:18 -04:00
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class FooComponent {}
|
|
|
|
`);
|
|
|
|
const location = mockHost.getLocationMarkerFor(fileName, 'model');
|
|
|
|
const completions = ngLS.getCompletionsAt(fileName, location.start) !;
|
|
|
|
expect(completions).toBeDefined();
|
2019-12-12 18:46:27 -05:00
|
|
|
const completion = completions.entries.find(entry => entry.name === 'ngModel') !;
|
2019-10-29 19:32:18 -04:00
|
|
|
expect(completion).toBeDefined();
|
2019-12-12 18:46:27 -05:00
|
|
|
expect(completion.kind).toBe(CompletionKind.ATTRIBUTE);
|
2019-10-29 19:32:18 -04:00
|
|
|
expect(completion.replacementSpan).toEqual({start: location.start - 5, length: 5});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
});
|
2019-12-12 18:46:27 -05:00
|
|
|
|
|
|
|
describe('property completions for members of an indexed type', () => {
|
|
|
|
it('should work with numeric index signatures (arrays)', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `{{ heroes[0].~{heroes-number-index}}}`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-number-index');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work with numeric index signatures (tuple arrays)', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `{{ tupleArray[1].~{tuple-array-number-index}}}`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'tuple-array-number-index');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('with string index signatures', () => {
|
|
|
|
it('should work with index notation', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `{{ heroesByName['Jacky'].~{heroes-string-index}}}`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work with dot notation', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `{{ heroesByName.jacky.~{heroes-string-index}}}`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'heroes-string-index');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work with dot notation if stringIndexType is a primitive type', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `{{ primitiveIndexType.test.~{string-primitive-type}}}`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'string-primitive-type');
|
|
|
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
|
|
|
expectContain(completions, CompletionKind.METHOD, ['substring']);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-10-24 21:29:24 -04:00
|
|
|
});
|
|
|
|
|
2019-10-14 18:24:28 -04:00
|
|
|
function expectContain(
|
|
|
|
completions: ts.CompletionInfo | undefined, kind: CompletionKind, names: string[]) {
|
|
|
|
expect(completions).toBeDefined();
|
2019-10-21 21:49:32 -04:00
|
|
|
for (const name of names) {
|
|
|
|
expect(completions !.entries).toContain(jasmine.objectContaining({ name, kind } as any));
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|