/** * @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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import * as ts from 'typescript/lib/tsserverlibrary'; import {LanguageServiceTestEnvironment} from './env'; function quickInfoSkeleton(): TestFile[] { return [ { name: absoluteFrom('/app.ts'), contents: ` import {Component, Directive, EventEmitter, Input, NgModule, Output, Pipe, PipeTransform} from '@angular/core'; import {CommonModule} from '@angular/common'; export interface Address { streetName: string; } /** The most heroic being. */ export interface Hero { id: number; name: string; address?: Address; } /** * This Component provides the \`test-comp\` selector. */ /*BeginTestComponent*/ @Component({ selector: 'test-comp', template: '
Testing: {{name}}
', }) export class TestComponent { @Input('tcName') name!: string; @Output('test') testEvent!: EventEmitter; } /*EndTestComponent*/ @Component({ selector: 'app-cmp', templateUrl: './app.html', }) export class AppCmp { hero!: Hero; heroes!: Hero[]; readonlyHeroes!: ReadonlyArray>; /** * This is the title of the \`AppCmp\` Component. */ title!: string; constNames!: [{readonly name: 'name'}]; birthday!: Date; anyValue!: any; myClick(event: any) {} setTitle(newTitle: string) {} trackByFn!: any; name!: any; } @Directive({ selector: '[string-model]', exportAs: 'stringModel', }) export class StringModel { @Input() model!: string; @Output() modelChange!: EventEmitter; } @Directive({selector: 'button[custom-button][compound]'}) export class CompoundCustomButtonDirective { @Input() config?: {color?: string}; } @NgModule({ declarations: [ AppCmp, CompoundCustomButtonDirective, StringModel, TestComponent, ], imports: [ CommonModule, ], }) export class AppModule {} `, isRoot: true, }, { name: absoluteFrom('/app.html'), contents: `Will be overridden`, } ]; } describe('quick info', () => { let env: LanguageServiceTestEnvironment; beforeEach(() => { initMockFileSystem('Native'); env = LanguageServiceTestEnvironment.setup(quickInfoSkeleton()); }); describe('elements', () => { it('should work for native elements', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: '', expectedDisplayString: '(element) button: HTMLButtonElement' }); }); it('should work for directives which match native element tags', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: '', expectedDisplayString: '(directive) AppModule.CompoundCustomButtonDirective' }); }); }); describe('templates', () => { it('should return undefined for ng-templates', () => { const {documentation} = expectQuickInfo({ templateOverride: ``, expectedSpanText: '', expectedDisplayString: '(template) ng-template' }); expect(toText(documentation)) .toContain('The `` is an Angular element for rendering HTML.'); }); }); describe('directives', () => { it('should work for directives', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'string-model', expectedDisplayString: '(directive) AppModule.StringModel' }); }); it('should work for components', () => { const {documentation} = expectQuickInfo({ templateOverride: ``, expectedSpanText: '', expectedDisplayString: '(component) AppModule.TestComponent' }); expect(toText(documentation)).toBe('This Component provides the `test-comp` selector.'); }); it('should work for structural directives', () => { const {documentation} = expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'ngFor', expectedDisplayString: '(directive) NgForOf' }); expect(toText(documentation)).toContain('A fake version of the NgFor directive.'); }); it('should work for directives with compound selectors, some of which are bindings', () => { expectQuickInfo({ templateOverride: `{{hero}}`, expectedSpanText: 'ngFor', expectedDisplayString: '(directive) NgForOf' }); }); it('should work for data-let- syntax', () => { expectQuickInfo({ templateOverride: `{{hero}}`, expectedSpanText: 'hero', expectedDisplayString: '(variable) hero: Hero' }); }); }); describe('bindings', () => { describe('inputs', () => { it('should work for input providers', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'tcName', expectedDisplayString: '(property) TestComponent.name: string' }); }); it('should work for bind- syntax', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'tcName', expectedDisplayString: '(property) TestComponent.name: string' }); expectQuickInfo({ templateOverride: ``, expectedSpanText: 'tcName', expectedDisplayString: '(property) TestComponent.name: string' }); }); it('should work for structural directive inputs ngForTrackBy', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'trackBy', expectedDisplayString: '(property) NgForOf.ngForTrackBy: TrackByFunction' }); }); it('should work for structural directive inputs ngForOf', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'of', expectedDisplayString: '(property) NgForOf.ngForOf: Hero[] | (Hero[] & Iterable) | null | undefined' }); }); it('should work for two-way binding providers', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'model', expectedDisplayString: '(property) StringModel.model: string' }); }); }); describe('outputs', () => { it('should work for event providers', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); }); it('should work for on- syntax binding', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); }); it('should work for $event from EventEmitter', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: '$event', expectedDisplayString: '(parameter) $event: string' }); }); it('should work for $event from native element', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: '$event', expectedDisplayString: '(parameter) $event: MouseEvent' }); }); }); }); describe('references', () => { it('should work for element reference declarations', () => { const {documentation} = expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'chart', expectedDisplayString: '(reference) chart: HTMLDivElement' }); expect(toText(documentation)) .toEqual( 'Provides special properties (beyond the regular HTMLElement ' + 'interface it also has available to it by inheritance) for manipulating
elements.'); }); it('should work for directive references', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'dirRef', expectedDisplayString: '(reference) dirRef: StringModel' }); }); it('should work for ref- syntax', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'chart', expectedDisplayString: '(reference) chart: HTMLDivElement' }); expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'chart', expectedDisplayString: '(reference) chart: HTMLDivElement' }); }); }); describe('variables', () => { it('should work for array members', () => { const {documentation} = expectQuickInfo({ templateOverride: `
{{her¦o}}
`, expectedSpanText: 'hero', expectedDisplayString: '(variable) hero: Hero' }); expect(toText(documentation)).toEqual('The most heroic being.'); }); it('should work for ReadonlyArray members (#36191)', () => { expectQuickInfo({ templateOverride: `
{{her¦o}}
`, expectedSpanText: 'hero', expectedDisplayString: '(variable) hero: Readonly' }); }); it('should work for const array members (#36191)', () => { expectQuickInfo({ templateOverride: `
{{na¦me}}
`, expectedSpanText: 'name', expectedDisplayString: '(variable) name: { readonly name: "name"; }' }); }); }); describe('pipes', () => { it('should work for pipes', () => { const templateOverride = `

The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}

`; expectQuickInfo({ templateOverride, expectedSpanText: 'date', expectedDisplayString: '(pipe) DatePipe.transform(value: string | number | Date, format?: string | undefined, timezone?: ' + 'string | undefined, locale?: string | undefined): string | null (+2 overloads)' }); }); }); describe('expressions', () => { it('should find members in a text interpolation', () => { expectQuickInfo({ templateOverride: `
{{ tit¦le }}
`, expectedSpanText: 'title', expectedDisplayString: '(property) AppCmp.title: string' }); }); it('should work for accessed property reads', () => { expectQuickInfo({ templateOverride: `
{{title.len¦gth}}
`, expectedSpanText: 'length', expectedDisplayString: '(property) String.length: number' }); }); it('should find members in an attribute interpolation', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'title', expectedDisplayString: '(property) AppCmp.title: string' }); }); it('should find members of input binding', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'title', expectedDisplayString: '(property) AppCmp.title: string' }); }); it('should find input binding on text attribute', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'tcName', expectedDisplayString: '(property) TestComponent.name: string' }); }); it('should find members of event binding', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'title', expectedDisplayString: '(property) AppCmp.title: string' }); }); it('should work for method calls', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'setTitle', expectedDisplayString: '(method) AppCmp.setTitle(newTitle: string): void' }); }); it('should work for accessed properties in writes', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'id', expectedDisplayString: '(property) Hero.id: number' }); }); it('should work for method call arguments', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'name', expectedDisplayString: '(property) Hero.name: string' }); }); it('should find members of two-way binding', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'title', expectedDisplayString: '(property) AppCmp.title: string' }); }); it('should find members in a structural directive', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'anyValue', expectedDisplayString: '(property) AppCmp.anyValue: any' }); }); it('should work for members in structural directives', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'heroes', expectedDisplayString: '(property) AppCmp.heroes: Hero[]' }); }); it('should work for the $any() cast function', () => { expectQuickInfo({ templateOverride: `
{{$an¦y(title)}}
`, expectedSpanText: '$any', expectedDisplayString: '(method) $any: any' }); }); it('should provide documentation', () => { const {cursor} = env.overrideTemplateWithCursor( absoluteFrom('/app.ts'), 'AppCmp', `
{{¦title}}
`); const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); const documentation = toText(quickInfo!.documentation); expect(documentation).toBe('This is the title of the `AppCmp` Component.'); }); }); function expectQuickInfo( {templateOverride, expectedSpanText, expectedDisplayString}: {templateOverride: string, expectedSpanText: string, expectedDisplayString: string}): ts.QuickInfo { const {cursor, text} = env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride); env.expectNoSourceDiagnostics(); env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp'); const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(text.substring(textSpan.start, textSpan.start + textSpan.length)) .toEqual(expectedSpanText); expect(toText(displayParts)).toEqual(expectedDisplayString); return quickInfo!; } }); function toText(displayParts?: ts.SymbolDisplayPart[]): string { return (displayParts || []).map(p => p.text).join(''); }