/**
* @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 {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import * as ts from 'typescript/lib/tsserverlibrary';
import {LanguageServiceTestEnv, Project} from '../testing';
function quickInfoSkeleton(): {[fileName: string]: string} {
return {
'app.ts': `
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 {}
`,
'app.html': `Will be overridden`,
};
}
describe('quick info', () => {
let env: LanguageServiceTestEnv;
let project: Project;
describe('strict templates (happy path)', () => {
beforeEach(() => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
project = env.addProject('test', 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 components with bound attributes', () => {
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[] & NgIterable) | 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
`;
template.moveCursorToText('{{¦title}}');
const quickInfo = template.getQuickInfoAtPosition();
const documentation = toText(quickInfo!.documentation);
expect(documentation).toBe('This is the title of the `AppCmp` Component.');
});
});
});
describe('generics', () => {
beforeEach(() => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
});
it('should get quick info for the generic input of a directive that normally requires inlining',
() => {
// When compiling normally, we would have to inline the type constructor of `GenericDir`
// because its generic type parameter references `PrivateInterface`, which is not exported.
project = env.addProject('test', {
'app.ts': `
import {Directive, Component, Input, NgModule} from '@angular/core';
interface PrivateInterface {}
@Directive({
selector: '[dir]'
})export class GenericDir {
@Input('input') input: T = null!;
}
@Component({
selector: 'some-cmp',
templateUrl: './app.html'
})export class SomeCmp{}
@NgModule({
declarations: [GenericDir, SomeCmp],
})export class AppModule{}
`,
'app.html': ``,
});
expectQuickInfo({
templateOverride: ``,
expectedSpanText: 'input',
expectedDisplayString: '(property) GenericDir.input: any'
});
});
});
describe('non-strict compiler options', () => {
beforeEach(() => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();
});
it('should find input binding on text attribute when strictAttributeTypes is false', () => {
project = env.addProject('test', quickInfoSkeleton(), {strictAttributeTypes: false});
expectQuickInfo({
templateOverride: ``,
expectedSpanText: 'tcName',
expectedDisplayString: '(property) TestComponent.name: string'
});
});
it('can still get quick info when strictOutputEventTypes is false', () => {
project = env.addProject('test', quickInfoSkeleton(), {strictOutputEventTypes: false});
expectQuickInfo({
templateOverride: ``,
expectedSpanText: 'test',
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter'
});
});
it('should work for pipes even if checkTypeOfPipes is false', () => {
// checkTypeOfPipes is set to false when strict templates is false
project = env.addProject('test', quickInfoSkeleton(), {strictTemplates: false});
const templateOverride = `
The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}