533 lines
19 KiB
TypeScript
533 lines
19 KiB
TypeScript
/**
|
|
* @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: '<div>Testing: {{name}}</div>',
|
|
})
|
|
export class TestComponent {
|
|
@Input('tcName') name!: string;
|
|
@Output('test') testEvent!: EventEmitter<string>;
|
|
} /*EndTestComponent*/
|
|
|
|
@Component({
|
|
selector: 'app-cmp',
|
|
templateUrl: './app.html',
|
|
})
|
|
export class AppCmp {
|
|
hero!: Hero;
|
|
heroes!: Hero[];
|
|
readonlyHeroes!: ReadonlyArray<Readonly<Hero>>;
|
|
/**
|
|
* 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<string>;
|
|
}
|
|
|
|
@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;
|
|
|
|
describe('strict templates (happy path)', () => {
|
|
beforeEach(() => {
|
|
initMockFileSystem('Native');
|
|
env = LanguageServiceTestEnvironment.setup(quickInfoSkeleton());
|
|
});
|
|
|
|
describe('elements', () => {
|
|
it('should work for native elements', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<butt¦on></button>`,
|
|
expectedSpanText: '<button></button>',
|
|
expectedDisplayString: '(element) button: HTMLButtonElement'
|
|
});
|
|
});
|
|
|
|
it('should work for directives which match native element tags', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<butt¦on compound custom-button></button>`,
|
|
expectedSpanText: '<button compound custom-button></button>',
|
|
expectedDisplayString: '(directive) AppModule.CompoundCustomButtonDirective'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('templates', () => {
|
|
it('should return undefined for ng-templates', () => {
|
|
const {documentation} = expectQuickInfo({
|
|
templateOverride: `<ng-templ¦ate></ng-template>`,
|
|
expectedSpanText: '<ng-template></ng-template>',
|
|
expectedDisplayString: '(template) ng-template'
|
|
});
|
|
expect(toText(documentation))
|
|
.toContain('The `<ng-template>` is an Angular element for rendering HTML.');
|
|
});
|
|
});
|
|
|
|
describe('directives', () => {
|
|
it('should work for directives', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div string-model¦></div>`,
|
|
expectedSpanText: 'string-model',
|
|
expectedDisplayString: '(directive) AppModule.StringModel'
|
|
});
|
|
});
|
|
|
|
it('should work for components', () => {
|
|
const {documentation} = expectQuickInfo({
|
|
templateOverride: `<t¦est-comp></test-comp>`,
|
|
expectedSpanText: '<test-comp></test-comp>',
|
|
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: `<div *¦ngFor="let item of heroes"></div>`,
|
|
expectedSpanText: 'ngFor',
|
|
expectedDisplayString: '(directive) NgForOf<Hero, Hero[]>'
|
|
});
|
|
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:
|
|
`<ng-template ngF¦or let-hero [ngForOf]="heroes">{{hero}}</ng-template>`,
|
|
expectedSpanText: 'ngFor',
|
|
expectedDisplayString: '(directive) NgForOf<Hero, Hero[]>'
|
|
});
|
|
});
|
|
|
|
it('should work for data-let- syntax', () => {
|
|
expectQuickInfo({
|
|
templateOverride:
|
|
`<ng-template ngFor data-let-he¦ro [ngForOf]="heroes">{{hero}}</ng-template>`,
|
|
expectedSpanText: 'hero',
|
|
expectedDisplayString: '(variable) hero: Hero'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('bindings', () => {
|
|
describe('inputs', () => {
|
|
it('should work for input providers', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp [tcN¦ame]="name"></test-comp>`,
|
|
expectedSpanText: 'tcName',
|
|
expectedDisplayString: '(property) TestComponent.name: string'
|
|
});
|
|
});
|
|
|
|
it('should work for bind- syntax', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp bind-tcN¦ame="name"></test-comp>`,
|
|
expectedSpanText: 'tcName',
|
|
expectedDisplayString: '(property) TestComponent.name: string'
|
|
});
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp data-bind-tcN¦ame="name"></test-comp>`,
|
|
expectedSpanText: 'tcName',
|
|
expectedDisplayString: '(property) TestComponent.name: string'
|
|
});
|
|
});
|
|
|
|
it('should work for structural directive inputs ngForTrackBy', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let item of heroes; tr¦ackBy: trackByFn;"></div>`,
|
|
expectedSpanText: 'trackBy',
|
|
expectedDisplayString:
|
|
'(property) NgForOf<Hero, Hero[]>.ngForTrackBy: TrackByFunction<Hero>'
|
|
});
|
|
});
|
|
|
|
it('should work for structural directive inputs ngForOf', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let item o¦f heroes; trackBy: trackByFn;"></div>`,
|
|
expectedSpanText: 'of',
|
|
expectedDisplayString:
|
|
'(property) NgForOf<Hero, Hero[]>.ngForOf: Hero[] | (Hero[] & Iterable<Hero>) | null | undefined'
|
|
});
|
|
});
|
|
|
|
it('should work for two-way binding providers', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp string-model [(mo¦del)]="title"></test-comp>`,
|
|
expectedSpanText: 'model',
|
|
expectedDisplayString: '(property) StringModel.model: string'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('outputs', () => {
|
|
it('should work for event providers', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp (te¦st)="myClick($event)"></test-comp>`,
|
|
expectedSpanText: 'test',
|
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
|
});
|
|
});
|
|
|
|
it('should work for on- syntax binding', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp on-te¦st="myClick($event)"></test-comp>`,
|
|
expectedSpanText: 'test',
|
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
|
});
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp data-on-te¦st="myClick($event)"></test-comp>`,
|
|
expectedSpanText: 'test',
|
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
|
});
|
|
});
|
|
|
|
it('should work for $event from EventEmitter', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div string-model (modelChange)="myClick($e¦vent)"></div>`,
|
|
expectedSpanText: '$event',
|
|
expectedDisplayString: '(parameter) $event: string'
|
|
});
|
|
});
|
|
|
|
it('should work for $event from native element', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (click)="myClick($e¦vent)"></div>`,
|
|
expectedSpanText: '$event',
|
|
expectedDisplayString: '(parameter) $event: MouseEvent'
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('references', () => {
|
|
it('should work for element reference declarations', () => {
|
|
const {documentation} = expectQuickInfo({
|
|
templateOverride: `<div #¦chart></div>`,
|
|
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 <div> elements.');
|
|
});
|
|
|
|
it('should work for directive references', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div string-model #dir¦Ref="stringModel"></div>`,
|
|
expectedSpanText: 'dirRef',
|
|
expectedDisplayString: '(reference) dirRef: StringModel'
|
|
});
|
|
});
|
|
|
|
it('should work for ref- syntax', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div ref-ch¦art></div>`,
|
|
expectedSpanText: 'chart',
|
|
expectedDisplayString: '(reference) chart: HTMLDivElement'
|
|
});
|
|
expectQuickInfo({
|
|
templateOverride: `<div data-ref-ch¦art></div>`,
|
|
expectedSpanText: 'chart',
|
|
expectedDisplayString: '(reference) chart: HTMLDivElement'
|
|
});
|
|
});
|
|
|
|
it('should work for $event from native element', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (click)="myClick($e¦vent)"></div>`,
|
|
expectedSpanText: '$event',
|
|
expectedDisplayString: '(parameter) $event: MouseEvent'
|
|
});
|
|
});
|
|
|
|
it('should work for click output from native element', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (cl¦ick)="myClick($event)"></div>`,
|
|
expectedSpanText: 'click',
|
|
expectedDisplayString:
|
|
'(event) HTMLDivElement.addEventListener<"click">(type: "click", ' +
|
|
'listener: (this: HTMLDivElement, ev: MouseEvent) => any, ' +
|
|
'options?: boolean | AddEventListenerOptions | undefined): void (+1 overload)'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('variables', () => {
|
|
it('should work for array members', () => {
|
|
const {documentation} = expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let hero of heroes">{{her¦o}}</div>`,
|
|
expectedSpanText: 'hero',
|
|
expectedDisplayString: '(variable) hero: Hero'
|
|
});
|
|
expect(toText(documentation)).toEqual('The most heroic being.');
|
|
});
|
|
|
|
it('should work for ReadonlyArray members (#36191)', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let hero of readonlyHeroes">{{her¦o}}</div>`,
|
|
expectedSpanText: 'hero',
|
|
expectedDisplayString: '(variable) hero: Readonly<Hero>'
|
|
});
|
|
});
|
|
|
|
it('should work for const array members (#36191)', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let name of constNames">{{na¦me}}</div>`,
|
|
expectedSpanText: 'name',
|
|
expectedDisplayString: '(variable) name: { readonly name: "name"; }'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('pipes', () => {
|
|
it('should work for pipes', () => {
|
|
const templateOverride = `<p>The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}</p>`;
|
|
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: `<div>{{ tit¦le }}</div>`,
|
|
expectedSpanText: 'title',
|
|
expectedDisplayString: '(property) AppCmp.title: string'
|
|
});
|
|
});
|
|
|
|
it('should work for accessed property reads', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div>{{title.len¦gth}}</div>`,
|
|
expectedSpanText: 'length',
|
|
expectedDisplayString: '(property) String.length: number'
|
|
});
|
|
});
|
|
|
|
it('should find members in an attribute interpolation', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div string-model model="{{tit¦le}}"></div>`,
|
|
expectedSpanText: 'title',
|
|
expectedDisplayString: '(property) AppCmp.title: string'
|
|
});
|
|
});
|
|
|
|
it('should find members of input binding', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp [tcName]="ti¦tle"></test-comp>`,
|
|
expectedSpanText: 'title',
|
|
expectedDisplayString: '(property) AppCmp.title: string'
|
|
});
|
|
});
|
|
|
|
it('should find input binding on text attribute', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp tcN¦ame="title"></test-comp>`,
|
|
expectedSpanText: 'tcName',
|
|
expectedDisplayString: '(property) TestComponent.name: string'
|
|
});
|
|
});
|
|
|
|
it('should find members of event binding', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp (test)="ti¦tle=$event"></test-comp>`,
|
|
expectedSpanText: 'title',
|
|
expectedDisplayString: '(property) AppCmp.title: string'
|
|
});
|
|
});
|
|
|
|
it('should work for method calls', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (click)="setT¦itle('title')"></div>`,
|
|
expectedSpanText: 'setTitle',
|
|
expectedDisplayString: '(method) AppCmp.setTitle(newTitle: string): void'
|
|
});
|
|
});
|
|
|
|
it('should work for accessed properties in writes', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (click)="hero.i¦d = 2"></div>`,
|
|
expectedSpanText: 'id',
|
|
expectedDisplayString: '(property) Hero.id: number'
|
|
});
|
|
});
|
|
|
|
it('should work for method call arguments', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div (click)="setTitle(hero.nam¦e)"></div>`,
|
|
expectedSpanText: 'name',
|
|
expectedDisplayString: '(property) Hero.name: string'
|
|
});
|
|
});
|
|
|
|
it('should find members of two-way binding', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<input string-model [(model)]="ti¦tle" />`,
|
|
expectedSpanText: 'title',
|
|
expectedDisplayString: '(property) AppCmp.title: string'
|
|
});
|
|
});
|
|
|
|
it('should find members in a structural directive', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngIf="anyV¦alue"></div>`,
|
|
expectedSpanText: 'anyValue',
|
|
expectedDisplayString: '(property) AppCmp.anyValue: any'
|
|
});
|
|
});
|
|
|
|
it('should work for members in structural directives', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div *ngFor="let item of her¦oes; trackBy: trackByFn;"></div>`,
|
|
expectedSpanText: 'heroes',
|
|
expectedDisplayString: '(property) AppCmp.heroes: Hero[]'
|
|
});
|
|
});
|
|
|
|
it('should work for the $any() cast function', () => {
|
|
expectQuickInfo({
|
|
templateOverride: `<div>{{$an¦y(title)}}</div>`,
|
|
expectedSpanText: '$any',
|
|
expectedDisplayString: '(method) $any: any'
|
|
});
|
|
});
|
|
|
|
it('should provide documentation', () => {
|
|
const {cursor} = env.overrideTemplateWithCursor(
|
|
absoluteFrom('/app.ts'), 'AppCmp', `<div>{{¦title}}</div>`);
|
|
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.');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('non-strict compiler options', () => {
|
|
it('should find input binding on text attribute when strictAttributeTypes is false', () => {
|
|
initMockFileSystem('Native');
|
|
env =
|
|
LanguageServiceTestEnvironment.setup(quickInfoSkeleton(), {strictAttributeTypes: false});
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp tcN¦ame="title"></test-comp>`,
|
|
expectedSpanText: 'tcName',
|
|
expectedDisplayString: '(property) TestComponent.name: string'
|
|
});
|
|
});
|
|
|
|
it('can still get quick info when strictOutputEventTypes is false', () => {
|
|
initMockFileSystem('Native');
|
|
env = LanguageServiceTestEnvironment.setup(
|
|
quickInfoSkeleton(), {strictOutputEventTypes: false});
|
|
expectQuickInfo({
|
|
templateOverride: `<test-comp (te¦st)="myClick($event)"></test-comp>`,
|
|
expectedSpanText: 'test',
|
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
|
});
|
|
});
|
|
});
|
|
|
|
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('');
|
|
}
|