/** * @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 * 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'; const PARSING_CASES = '/app/parsing-cases.ts'; 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(); }); describe('location of hover', () => { it('should find members in a text interpolation', () => { mockHost.override(TEST_TEMPLATE, '
{{«title»}}
'); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should find members in an attribute interpolation', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should find members in a property binding', () => { mockHost.override(TEST_TEMPLATE, ``); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should find members in an event binding', () => { mockHost.override(TEST_TEMPLATE, ``); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should find members in a two-way binding', () => { mockHost.override(TEST_TEMPLATE, ``); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should find members in a structural directive', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'anyValue'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.anyValue: any'); }); }); describe('hovering on expression nodes', () => { it('should provide documentation', () => { mockHost.override(TEST_TEMPLATE, `
{{~{cursor}title}}
`); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeDefined(); const documentation = toText(quickInfo!.documentation); expect(documentation).toBe('This is the title of the `TemplateReference` Component.'); }); describe('property reads', () => { it('should work for class members', () => { mockHost.override(TEST_TEMPLATE, `
{{«title»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should work for accessed property reads', () => { mockHost.override(TEST_TEMPLATE, `
{{title.«length»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'length'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) String.length: number'); }); it('should work for properties in writes', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); it('should work for accessed properties in writes', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'id'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) Hero.id: number'); }); it('should work for array members', () => { mockHost.override(TEST_TEMPLATE, `
{{«hero»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'hero'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(variable) hero: Hero'); }); it('should work for ReadonlyArray members (#36191)', () => { mockHost.override( TEST_TEMPLATE, `
{{«hero»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'hero'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(variable) hero: Readonly'); }); it('should work for const array members (#36191)', () => { mockHost.override(TEST_TEMPLATE, `
{{«name»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'name'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(variable) name: { readonly name: "name"; }'); }); }); it('should work for method calls', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'myClick'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(method) TemplateReference.myClick: (event: any) => void'); }); it('should work for structural directive inputs', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'trackBy'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(method) NgForOf.ngForTrackBy: TrackByFunction'); }); it('should work for members in structural directives', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'heroes'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.heroes: Hero[]'); }); it('should work for pipes', () => { mockHost.override(TEST_TEMPLATE, `

The hero's birthday is {{birthday | «date»: "MM/dd/yy"}}

`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'date'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)) .toBe( '(pipe) date: { (value: string | number | Date, format?: string | undefined, timezone?: string | undefined, locale?: string | undefined): string | null; (value: null | undefined, format?: string | undefined, timezone?: string | undefined, locale?: string | undefined): null; (value: string | ... 3 more ... | undefined, format?: st...'); }); it('should work for the $any() cast function', () => { const content = mockHost.override(TEST_TEMPLATE, '
{{$any(title)}}
'); const position = content.indexOf('$any'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, position); expect(quickInfo).toBeDefined(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual({ start: position, length: '$any'.length, }); expect(toText(displayParts)).toBe('(method) $any: $any'); }); }); describe('hovering on template nodes', () => { it('should provide documentation', () => { mockHost.override(TEST_TEMPLATE, `<~{cursor}test-comp>`); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeDefined(); const documentation = toText(quickInfo!.documentation); expect(documentation).toBe('This Component provides the `test-comp` selector.'); }); it('should work for components', () => { mockHost.override(TEST_TEMPLATE, '<~{cursor}test-comp>'); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeDefined(); const {displayParts, documentation} = quickInfo!; expect(toText(displayParts)) .toBe('(component) AppModule.TestComponent: typeof TestComponent'); expect(toText(documentation)).toBe('This Component provides the `test-comp` selector.'); }); it('should work for directives', () => { const content = mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeDefined(); const {displayParts, textSpan} = quickInfo!; expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: typeof StringModel'); expect(content.substring(textSpan.start, textSpan.start + textSpan.length)) .toBe('string-model'); }); it('should work for event providers', () => { mockHost.override(TEST_TEMPLATE, ``); const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'test'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(event) TestComponent.testEvent: EventEmitter'); }); it('should work for input providers', () => { mockHost.override(TEST_TEMPLATE, ``); const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'tcName'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TestComponent.name: string'); }); it('should work for two-way binding providers', () => { mockHost.override( TEST_TEMPLATE, ``); const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'model'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) StringModel.model: string'); }); it('should work for structural directives', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'ngFor'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(directive) NgForOf: typeof NgForOf'); }); }); describe('hovering on TypeScript nodes', () => { it('should work for component TypeScript declarations', () => { const content = mockHost.readFile(PARSING_CASES)!; const position = content.indexOf('TemplateReference'); expect(position).toBeGreaterThan(0); const quickInfo = ngLS.getQuickInfoAtPosition(PARSING_CASES, position); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual({ start: position, length: 'TemplateReference'.length, }); expect(toText(displayParts)).toBe('(component) AppModule.TemplateReference: class'); }); it('should work for directive TypeScript declarations', () => { const content = mockHost.readFile(PARSING_CASES)!; const position = content.indexOf('StringModel'); expect(position).toBeGreaterThan(0); const quickInfo = ngLS.getQuickInfoAtPosition(PARSING_CASES, position); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual({ start: position, length: 'StringModel'.length, }); expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class'); }); }); describe('non-goals', () => { it('should ignore reference declarations', () => { mockHost.override(TEST_TEMPLATE, `
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'chart'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeUndefined(); }); it('should not expand i18n templates', () => { mockHost.override(TEST_TEMPLATE, `
{{«title»}}
`); const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title'); const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(textSpan).toEqual(marker); expect(toText(displayParts)).toBe('(property) TemplateReference.title: string'); }); }); }); function toText(displayParts?: ts.SymbolDisplayPart[]): string { return (displayParts || []).map(p => p.text).join(''); }