/** * @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 {assertFileNames, assertTextSpans, createModuleAndProjectWithDeclarations, humanizeDocumentSpanLike, LanguageServiceTestEnv, OpenBuffer} from '../testing'; describe('find references and rename locations', () => { let env: LanguageServiceTestEnv; beforeEach(() => { initMockFileSystem('Native'); }); afterEach(() => { // Clear env so it's not accidentally carried over to the next test. env = undefined!; }); describe('cursor is on binding in component class', () => { let appFile: OpenBuffer; beforeEach(() => { const files = { 'app.ts': `import {Component} from '@angular/core'; @Component({templateUrl: './app.html'}) export class AppCmp { myProp!: string; }`, 'app.html': '{{myProp}}' }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); appFile = project.openFile('app.ts'); appFile.moveCursorToText('myP¦rop'); }); it('gets component member references from TS file and external template', () => { const refs = getReferencesAtPosition(appFile)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); assertTextSpans(refs, ['myProp']); }); it('gets rename locations from TS file and external template', () => { const renameLocations = getRenameLocationsAtPosition(appFile)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.html', 'app.ts']); assertTextSpans(renameLocations, ['myProp']); }); }); describe('when cursor is on binding in an external template', () => { let templateFile: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html'}) export class AppCmp { myProp = ''; }`, 'app.html': '{{myProp}}' }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); templateFile = project.openFile('app.html'); templateFile.moveCursorToText('myP¦rop'); }); it('gets references', () => { const refs = getReferencesAtPosition(templateFile)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); assertTextSpans(refs, ['myProp']); }); it('gets rename locations', () => { const renameLocations = getRenameLocationsAtPosition(templateFile)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.html', 'app.ts']); assertTextSpans(renameLocations, ['myProp']); }); }); describe('when cursor is on function call in external template', () => { let appFile: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: '
'}) export class AppCmp { setTitle(s: number) {} }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); appFile = project.openFile('app.ts'); appFile.moveCursorToText('setTi¦tle(2)'); }); it('gets component member reference in ts file', () => { const refs = getReferencesAtPosition(appFile)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); assertTextSpans(refs, ['setTitle']); }); it('gets rename location in ts file', () => { const renameLocations = getRenameLocationsAtPosition(appFile)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.ts']); assertTextSpans(renameLocations, ['setTitle']); }); }); describe('when cursor in on argument to a function call in an external template', () => { let appFile: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: ''}) export class AppCmp { title = ''; setTitle(s: string) {} }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); appFile = project.openFile('app.ts'); appFile.moveCursorToText('(ti¦tle)'); }); it('gets member reference in ts file', () => { const refs = getReferencesAtPosition(appFile)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['title']); }); it('finds rename location in ts file', () => { const refs = getRenameLocationsAtPosition(appFile)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['title']); }); }); describe('when cursor is on $event in method call arguments', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: ''}) export class AppCmp { setTitle(s: any) {} }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.ts'); file.moveCursorToText('($even¦t)'); }); it('find references', () => { const refs = getReferencesAtPosition(file)!; expect(refs.length).toBe(1); assertTextSpans(refs, ['$event']); }); it('gets no rename locations', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations).toBeUndefined(); }); }); describe('when cursor in on LHS of property write in external template', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html' }) export class AppCmp { title = ''; }`, 'app.html': `` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.html'); file.moveCursorToText('ti¦tle = '); }); it('gets member reference in ts file', () => { const refs = getReferencesAtPosition(file)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts', 'app.html']); assertTextSpans(refs, ['title']); }); it('gets rename location in ts file', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.ts', 'app.html']); assertTextSpans(renameLocations, ['title']); }); }); describe('when cursor in on RHS of property write in external template', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: '' }) export class AppCmp { title = ''; otherTitle = ''; }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.ts'); file.moveCursorToText('= otherT¦itle">'); }); it('get reference to member in ts file', () => { const refs = getReferencesAtPosition(file)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); assertTextSpans(refs, ['otherTitle']); }); it('finds rename location in ts file', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.ts']); assertTextSpans(renameLocations, ['otherTitle']); }); }); describe('when cursor in on a keyed read', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: '{{hero["name"]}}' }) export class AppCmp { hero: {name: string} = {name: 'Superman'}; }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.ts'); file.moveCursorToText('{{hero["na¦me"]}}'); }); it('gets reference to member type definition and initialization in component class', () => { const refs = getReferencesAtPosition(file)!; // 3 references: the type definition, the value assignment, and the read in the template expect(refs.length).toBe(3); assertFileNames(refs, ['app.ts']); // TODO(atscott): investigate if we can make the template keyed read be just the 'name' part. // The TypeScript implementation specifically adjusts the span to accommodate string literals: // https://sourcegraph.com/github.com/microsoft/TypeScript@d5779c75d3dd19565b60b9e2960b8aac36d4d635/-/blob/src/services/findAllReferences.ts#L508-512 // One possible solution would be to extend `FullTemplateMapping` to include the matched TCB // node and then do the same thing that TS does: if the node is a string, adjust the span. assertTextSpans(refs, ['name', '"name"']); }); it('gets rename locations in component class', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations).toBeUndefined(); // TODO(atscott): We should handle this case. The fix requires us to fix the result span as // described above. // 3 references: the type definition, the value assignment, and the read in the template // expect(renameLocations.length).toBe(3); // // assertFileNames(renameLocations, ['app.ts']); // assertTextSpans(renameLocations, ['name']); }); }); describe('when cursor in on RHS of keyed write in a template', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html' }) export class AppCmp { hero: {name: string} = {name: 'Superman'}; batman = 'batman'; }`, 'app.html': `` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.html'); file.moveCursorToText('bat¦man'); }); it('get references in ts file', () => { const refs = getReferencesAtPosition(file)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts', 'app.html']); assertTextSpans(refs, ['batman']); }); it('finds rename location in ts file', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations.length).toBe(2); assertFileNames(renameLocations, ['app.ts', 'app.html']); assertTextSpans(renameLocations, ['batman']); }); }); describe('when cursor in on an element reference', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({template: ' {{ myInput.value }}'}) export class AppCmp { title = ''; }` }; env = LanguageServiceTestEnv.setup(); const project = createModuleAndProjectWithDeclarations(env, 'test', files); file = project.openFile('app.ts'); file.moveCursorToText('myInp¦ut.value'); }); it('get reference to declaration in template', () => { const refs = getReferencesAtPosition(file)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['myInput']); // Get the declaration by finding the reference that appears first in the template refs.sort((a, b) => a.textSpan.start - b.textSpan.start); expect(refs[0].isDefinition).toBe(true); }); it('finds rename location in template', () => { const renameLocations = getRenameLocationsAtPosition(file)!; expect(renameLocations.length).toBe(2); assertTextSpans(renameLocations, ['myInput']); }); }); describe('when cursor in on a template reference', () => { let file: OpenBuffer; beforeEach(() => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html'}) export class AppCmp { title = ''; }`, 'app.html': `