/** * @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'; 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', () => { 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', () => { 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', () => { 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.HTML_ELEMENT, ['div', 'h1', 'h2', 'span']); } }); it('should be able to return component 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 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']); }); 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', ]); }); 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 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', ]); }); 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(); }); 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']); }); 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 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.HTML_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.HTML_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.HTML_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']); }); 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']); }); it('should suggest attribute values', () => { mockHost.override(TEST_TEMPLATE, `
`); 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, ``); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); expectContain(completions, CompletionKind.METHOD, ['myClick']); }); 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 not 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); 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'})); } }); it('should be able to return element directives', () => { 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', ]); }); it('should not return html attributes', () => { const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'h1-after-space'); const completions = ngLS.getCompletionsAt(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', () => { const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'div-attributes'); const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start); expectContain(completions, CompletionKind.ATTRIBUTE, [ '(click)', '[ngClass]', '*ngIf', '*ngFor', ]); }); }); 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']); }); }); 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']); }); it('should be able to resolve variable in nested loop', () => { mockHost.override(TEST_TEMPLATE, `