The compiler's `I18NHtmlParser` may expand template nodes that have internationalization metadata attached to them; for instance, ```html <div i18n="@@i18n-el">{{}}</div> ``` gets expanded to an AST with the i18n metadata extracted and text filled in as necessary; to the language service, the template above, as read in the AST, now looks something like ```html <div>{{$implicit}}</div> ``` This is undesirable for the language service because we want to preserve the original form of the source template source code, and have information about the original values of the template. The language service also does not need to use an i18n parser -- we don't generate any template output. To fix this turns out to be as easy as moving to using a raw `HtmlParser`. --- A note on the testing strategy: as mentioned above, we don't need to use an i18n parser, but we don't **not** need to use one if the parser does not heavily modify the template AST. For this reason, the tests target the functionality of not modifying a template with i18n metadata rather than testing that the language service does not use an i18n parser. --- Closes https://github.com/angular/vscode-ng-language-service/issues/272 PR Close #34531
247 lines
9.2 KiB
TypeScript
247 lines
9.2 KiB
TypeScript
/**
|
|
* @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 {TypeScriptServiceHost} from '../src/typescript_host';
|
|
|
|
import {MockTypescriptHost} from './test_utils';
|
|
|
|
const TEST_TEMPLATE = '/app/test.ng';
|
|
|
|
describe('hover', () => {
|
|
const mockHost = new MockTypescriptHost(['/app/main.ts']);
|
|
const tsLS = ts.createLanguageService(mockHost);
|
|
const ngLSHost = new TypeScriptServiceHost(mockHost, tsLS);
|
|
const ngLS = createLanguageService(ngLSHost);
|
|
|
|
beforeEach(() => { mockHost.reset(); });
|
|
|
|
it('should be able to find field in an interpolation', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '{{«name»}}'
|
|
})
|
|
export class MyComponent {
|
|
name: string;
|
|
}`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
|
});
|
|
|
|
it('should be able to find a field in a attribute reference', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<input [(ngModel)]="«name»">'
|
|
})
|
|
export class MyComponent {
|
|
name: string;
|
|
}`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
|
});
|
|
|
|
it('should be able to find a method from a call', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<div (click)="«ᐱmyClickᐱ()»;"></div>'
|
|
})
|
|
export class MyComponent {
|
|
myClick() { }
|
|
}`);
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'myClick');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(textSpan.length).toBe('myClick()'.length);
|
|
expect(toText(displayParts)).toBe('(method) MyComponent.myClick: () => void');
|
|
});
|
|
|
|
it('should be able to find a field reference in an *ngIf', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<div *ngIf="«include»"></div>'
|
|
})
|
|
export class MyComponent {
|
|
include = true;
|
|
}`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'include');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.include: boolean');
|
|
});
|
|
|
|
it('should be able to find a reference to a component', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '«<ᐱtestᐱ-comp></test-comp>»'
|
|
})
|
|
export class MyComponent { }`);
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'test');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(component) AppModule.TestComponent: class');
|
|
});
|
|
|
|
it('should be able to find a reference to a directive', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<test-comp «string-model»></test-comp>'
|
|
})
|
|
export class MyComponent { }`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'string-model');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(directive) StringModel: typeof StringModel');
|
|
});
|
|
|
|
it('should be able to find an event provider', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<test-comp «(ᐱtestᐱ)="myHandler()"»></div>'
|
|
})
|
|
export class MyComponent {
|
|
myHandler() {}
|
|
}`);
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'test');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(event) TestComponent.testEvent: EventEmitter<any>');
|
|
});
|
|
|
|
it('should be able to find an input provider', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<test-comp «[ᐱtcNameᐱ]="name"»></div>'
|
|
})
|
|
export class MyComponent {
|
|
name = 'my name';
|
|
}`);
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'tcName');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(property) TestComponent.name: string');
|
|
});
|
|
|
|
it('should be able to ignore a reference declaration', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<div #«chart»></div>'
|
|
})
|
|
export class MyComponent { }`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'chart');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeUndefined();
|
|
});
|
|
|
|
it('should be able to find the NgModule of a component', () => {
|
|
const fileName = '/app/app.component.ts';
|
|
mockHost.override(fileName, `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
template: '<div></div>'
|
|
})
|
|
export class «AppComponent» {
|
|
name: string;
|
|
}`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'AppComponent');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(component) AppModule.AppComponent: class');
|
|
});
|
|
|
|
it('should be able to find the NgModule of a directive', () => {
|
|
const fileName = '/app/parsing-cases.ts';
|
|
const content = mockHost.readFile(fileName) !;
|
|
const position = content.indexOf('StringModel');
|
|
expect(position).toBeGreaterThan(0);
|
|
const quickInfo = ngLS.getHoverAt(fileName, position);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual({
|
|
start: position,
|
|
length: 'StringModel'.length,
|
|
});
|
|
expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class');
|
|
});
|
|
|
|
it('should be able to provide quick info for $any() cast function', () => {
|
|
const content = mockHost.override(TEST_TEMPLATE, '<div>{{$any(title)}}</div>');
|
|
const position = content.indexOf('$any');
|
|
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, position);
|
|
expect(quickInfo).toBeDefined();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual({
|
|
start: position,
|
|
length: '$any(title)'.length,
|
|
});
|
|
expect(toText(displayParts)).toBe('(method) $any: $any');
|
|
});
|
|
|
|
it('should provide documentation for a property', () => {
|
|
mockHost.override(TEST_TEMPLATE, `<div>{{~{cursor}title}}</div>`);
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
|
|
expect(quickInfo).toBeDefined();
|
|
const documentation = toText(quickInfo !.documentation);
|
|
expect(documentation).toBe('This is the title of the `TemplateReference` Component.');
|
|
});
|
|
|
|
it('should provide documentation for a selector', () => {
|
|
mockHost.override(TEST_TEMPLATE, `<~{cursor}test-comp></test-comp>`);
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
|
|
expect(quickInfo).toBeDefined();
|
|
const documentation = toText(quickInfo !.documentation);
|
|
expect(documentation).toBe('This Component provides the `test-comp` selector.');
|
|
});
|
|
|
|
it('should not expand i18n templates', () => {
|
|
const fileName = mockHost.addCode(`
|
|
@Component({
|
|
template: '<div i18n="@@el">{{«name»}}</div>'
|
|
})
|
|
export class MyComponent {
|
|
name: string;
|
|
}`);
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
|
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
|
expect(quickInfo).toBeTruthy();
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
expect(textSpan).toEqual(marker);
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
|
});
|
|
});
|
|
|
|
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
|
return (displayParts || []).map(p => p.text).join('');
|
|
}
|