refactor(language-service): migrate definitions_spec to new testing package (#40966)
refactor(language-service): migrate definitions_spec to new testing package PR Close #40966
This commit is contained in:
		
							parent
							
								
									8808002e54
								
							
						
					
					
						commit
						d2b43d577b
					
				| @ -6,102 +6,100 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; | ||||
| import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; | ||||
| 
 | ||||
| import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; | ||||
| import {assertFileNames, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils'; | ||||
| import {assertFileNames, createModuleAndProjectWithDeclarations, humanizeDocumentSpanLike, LanguageServiceTestEnv, OpenBuffer} from '../testing'; | ||||
| 
 | ||||
| describe('definitions', () => { | ||||
|   it('gets definition for template reference in overridden template', () => { | ||||
|     initMockFileSystem('Native'); | ||||
|     const templateFile = {contents: '', name: absoluteFrom('/app.html')}; | ||||
|     const appFile = { | ||||
|       name: absoluteFrom('/app.ts'), | ||||
|       contents: ` | ||||
|     const files = { | ||||
|       'app.html': '', | ||||
|       'app.ts': ` | ||||
|         import {Component} from '@angular/core'; | ||||
| 
 | ||||
|         @Component({templateUrl: '/app.html'}) | ||||
|         export class AppCmp {} | ||||
|       `,
 | ||||
|     }; | ||||
|     const env = LanguageServiceTestEnv.setup(); | ||||
| 
 | ||||
|     const env = createModuleWithDeclarations([appFile], [templateFile]); | ||||
|     const {cursor} = env.updateFileWithCursor( | ||||
|         absoluteFrom('/app.html'), '<input #myInput /> {{myIn¦put.value}}'); | ||||
|     env.expectNoSourceDiagnostics(); | ||||
|     const {definitions} = env.ngLS.getDefinitionAndBoundSpan(absoluteFrom('/app.html'), cursor)!; | ||||
|     const project = createModuleAndProjectWithDeclarations(env, 'test', files); | ||||
|     const template = project.openFile('app.html'); | ||||
|     template.contents = '<input #myInput /> {{myInput.value}}'; | ||||
|     project.expectNoSourceDiagnostics(); | ||||
| 
 | ||||
|     template.moveCursorToText('{{myIn¦put.value}}'); | ||||
|     const {definitions} = getDefinitionsAndAssertBoundSpan(env, template); | ||||
|     expect(definitions![0].name).toEqual('myInput'); | ||||
|     assertFileNames(Array.from(definitions!), ['app.html']); | ||||
|   }); | ||||
| 
 | ||||
|   it('returns the pipe class as definition when checkTypeOfPipes is false', () => { | ||||
|     initMockFileSystem('Native'); | ||||
|     const {cursor, text} = extractCursorInfo('{{"1/1/2020" | dat¦e}}'); | ||||
|     const templateFile = {contents: text, name: absoluteFrom('/app.html')}; | ||||
|     const appFile = { | ||||
|       name: absoluteFrom('/app.ts'), | ||||
|       contents: ` | ||||
|     const files = { | ||||
|       'app.ts': ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         import {CommonModule} from '@angular/common'; | ||||
| 
 | ||||
|         @Component({templateUrl: 'app.html'}) | ||||
|         export class AppCmp {} | ||||
|       `,
 | ||||
|       'app.html': '{{"1/1/2020" | date}}' | ||||
|     }; | ||||
|     // checkTypeOfPipes is set to false when strict templates is false
 | ||||
|     const env = createModuleWithDeclarations([appFile], [templateFile], {strictTemplates: false}); | ||||
|     const {textSpan, definitions} = | ||||
|         getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); | ||||
|     expect(text.substr(textSpan.start, textSpan.length)).toEqual('date'); | ||||
|     const env = LanguageServiceTestEnv.setup(); | ||||
|     const project = | ||||
|         createModuleAndProjectWithDeclarations(env, 'test', files, {strictTemplates: false}); | ||||
|     const template = project.openFile('app.html'); | ||||
|     project.expectNoSourceDiagnostics(); | ||||
|     template.moveCursorToText('da¦te'); | ||||
| 
 | ||||
|     expect(definitions.length).toEqual(1); | ||||
|     const [def] = definitions; | ||||
|     const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template); | ||||
|     expect(template.contents.substr(textSpan.start, textSpan.length)).toEqual('date'); | ||||
|     expect(definitions!.length).toEqual(1); | ||||
|     const [def] = definitions!; | ||||
|     expect(def.textSpan).toContain('DatePipe'); | ||||
|     expect(def.contextSpan).toContain('DatePipe'); | ||||
|   }); | ||||
| 
 | ||||
|   it('gets definitions for all inputs when attribute matches more than one', () => { | ||||
|     initMockFileSystem('Native'); | ||||
|     const {cursor, text} = extractCursorInfo('<div dir inpu¦tA="abc"></div>'); | ||||
|     const templateFile = {contents: text, name: absoluteFrom('/app.html')}; | ||||
|     const dirFile = { | ||||
|       name: absoluteFrom('/dir.ts'), | ||||
|       contents: ` | ||||
|     const files = { | ||||
|       'app.ts': ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         import {CommonModule} from '@angular/common'; | ||||
| 
 | ||||
|         @Component({templateUrl: 'app.html'}) | ||||
|         export class AppCmp {} | ||||
|       `,
 | ||||
|       'app.html': '<div dir inputA="abc"></div>', | ||||
|       'dir.ts': ` | ||||
|       import {Directive, Input} from '@angular/core'; | ||||
| 
 | ||||
|       @Directive({selector: '[dir]'}) | ||||
|       export class MyDir { | ||||
|         @Input() inputA!: any; | ||||
|       }`,
 | ||||
|     }; | ||||
|     const dirFile2 = { | ||||
|       name: absoluteFrom('/dir2.ts'), | ||||
|       contents: ` | ||||
|       'dir2.ts': ` | ||||
|       import {Directive, Input} from '@angular/core'; | ||||
| 
 | ||||
|       @Directive({selector: '[dir]'}) | ||||
|       export class MyDir2 { | ||||
|         @Input() inputA!: any; | ||||
|       }`,
 | ||||
|     }; | ||||
|     const appFile = { | ||||
|       name: absoluteFrom('/app.ts'), | ||||
|       contents: ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         import {CommonModule} from '@angular/common'; | ||||
|       }` | ||||
| 
 | ||||
|         @Component({templateUrl: 'app.html'}) | ||||
|         export class AppCmp {} | ||||
|       ` | ||||
|     }; | ||||
|     const env = createModuleWithDeclarations([appFile, dirFile, dirFile2], [templateFile]); | ||||
|     const {textSpan, definitions} = | ||||
|         getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); | ||||
|     expect(text.substr(textSpan.start, textSpan.length)).toEqual('inputA'); | ||||
|     const env = LanguageServiceTestEnv.setup(); | ||||
|     const project = createModuleAndProjectWithDeclarations(env, 'test', files); | ||||
|     const template = project.openFile('app.html'); | ||||
|     template.moveCursorToText('inpu¦tA="abc"'); | ||||
| 
 | ||||
|     expect(definitions.length).toEqual(2); | ||||
|     const [def, def2] = definitions; | ||||
|     const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template); | ||||
|     expect(template.contents.substr(textSpan.start, textSpan.length)).toEqual('inputA'); | ||||
| 
 | ||||
|     expect(definitions!.length).toEqual(2); | ||||
|     const [def, def2] = definitions!; | ||||
|     expect(def.textSpan).toContain('inputA'); | ||||
|     expect(def2.textSpan).toContain('inputA'); | ||||
|     // TODO(atscott): investigate why the text span includes more than just 'inputA'
 | ||||
| @ -111,31 +109,23 @@ describe('definitions', () => { | ||||
| 
 | ||||
|   it('gets definitions for all outputs when attribute matches more than one', () => { | ||||
|     initMockFileSystem('Native'); | ||||
|     const {cursor, text} = extractCursorInfo('<div dir (someEv¦ent)="doSomething()"></div>'); | ||||
|     const templateFile = {contents: text, name: absoluteFrom('/app.html')}; | ||||
|     const dirFile = { | ||||
|       name: absoluteFrom('/dir.ts'), | ||||
|       contents: ` | ||||
|     const files = { | ||||
|       'app.html': '<div dir (someEvent)="doSomething()"></div>', | ||||
|       'dir.ts': ` | ||||
|       import {Directive, Output, EventEmitter} from '@angular/core'; | ||||
| 
 | ||||
|       @Directive({selector: '[dir]'}) | ||||
|       export class MyDir { | ||||
|         @Output() someEvent = new EventEmitter<void>(); | ||||
|       }`,
 | ||||
|     }; | ||||
|     const dirFile2 = { | ||||
|       name: absoluteFrom('/dir2.ts'), | ||||
|       contents: ` | ||||
|       'dir2.ts': ` | ||||
|       import {Directive, Output, EventEmitter} from '@angular/core'; | ||||
| 
 | ||||
|       @Directive({selector: '[dir]'}) | ||||
|       export class MyDir2 { | ||||
|         @Output() someEvent = new EventEmitter<void>(); | ||||
|       }`,
 | ||||
|     }; | ||||
|     const appFile = { | ||||
|       name: absoluteFrom('/app.ts'), | ||||
|       contents: ` | ||||
|       'app.ts': ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         import {CommonModule} from '@angular/common'; | ||||
| 
 | ||||
| @ -145,10 +135,13 @@ describe('definitions', () => { | ||||
|         } | ||||
|       ` | ||||
|     }; | ||||
|     const env = createModuleWithDeclarations([appFile, dirFile, dirFile2], [templateFile]); | ||||
|     const {textSpan, definitions} = | ||||
|         getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); | ||||
|     expect(text.substr(textSpan.start, textSpan.length)).toEqual('someEvent'); | ||||
|     const env = LanguageServiceTestEnv.setup(); | ||||
|     const project = createModuleAndProjectWithDeclarations(env, 'test', files); | ||||
|     const template = project.openFile('app.html'); | ||||
|     template.moveCursorToText('(someEv¦ent)'); | ||||
| 
 | ||||
|     const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template); | ||||
|     expect(template.contents.substr(textSpan.start, textSpan.length)).toEqual('someEvent'); | ||||
| 
 | ||||
|     expect(definitions.length).toEqual(2); | ||||
|     const [def, def2] = definitions; | ||||
| @ -159,10 +152,9 @@ describe('definitions', () => { | ||||
|     assertFileNames([def, def2], ['dir2.ts', 'dir.ts']); | ||||
|   }); | ||||
| 
 | ||||
|   function getDefinitionsAndAssertBoundSpan( | ||||
|       env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) { | ||||
|   function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) { | ||||
|     env.expectNoSourceDiagnostics(); | ||||
|     const definitionAndBoundSpan = env.ngLS.getDefinitionAndBoundSpan(fileName, cursor); | ||||
|     const definitionAndBoundSpan = file.getDefinitionAndBoundSpan(); | ||||
|     const {textSpan, definitions} = definitionAndBoundSpan!; | ||||
|     expect(definitions).toBeTruthy(); | ||||
|     return {textSpan, definitions: definitions!.map(d => humanizeDocumentSpanLike(d, env))}; | ||||
|  | ||||
| @ -12,7 +12,7 @@ import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; | ||||
| import * as ts from 'typescript/lib/tsserverlibrary'; | ||||
| 
 | ||||
| import {MockServerHost} from './host'; | ||||
| import {Project, ProjectFiles} from './project'; | ||||
| import {Project, ProjectFiles, TestableOptions} from './project'; | ||||
| 
 | ||||
| /** | ||||
|  * Testing environment for the Angular Language Service, which creates an in-memory tsserver | ||||
| @ -48,12 +48,12 @@ export class LanguageServiceTestEnv { | ||||
| 
 | ||||
|   constructor(private host: MockServerHost, private projectService: ts.server.ProjectService) {} | ||||
| 
 | ||||
|   addProject(name: string, files: ProjectFiles): Project { | ||||
|   addProject(name: string, files: ProjectFiles, options: TestableOptions = {}): Project { | ||||
|     if (this.projects.has(name)) { | ||||
|       throw new Error(`Project ${name} is already defined`); | ||||
|     } | ||||
| 
 | ||||
|     const project = Project.initialize(name, this.projectService, files); | ||||
|     const project = Project.initialize(name, this.projectService, files, options); | ||||
|     this.projects.set(name, project); | ||||
|     return project; | ||||
|   } | ||||
| @ -65,6 +65,12 @@ export class LanguageServiceTestEnv { | ||||
|     } | ||||
|     return scriptInfo.getSnapshot().getText(span.start, span.start + span.length); | ||||
|   } | ||||
| 
 | ||||
|   expectNoSourceDiagnostics(): void { | ||||
|     for (const project of this.projects.values()) { | ||||
|       project.expectNoSourceDiagnostics(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const logger: ts.server.Logger = { | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
| 
 | ||||
| import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; | ||||
| import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; | ||||
| import { OptimizeFor } from '@angular/compiler-cli/src/ngtsc/typecheck/api'; | ||||
| import {OptimizeFor} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; | ||||
| import * as ts from 'typescript/lib/tsserverlibrary'; | ||||
| import {LanguageService} from '../../language_service'; | ||||
| import {OpenBuffer} from './buffer'; | ||||
| @ -44,6 +44,7 @@ function writeTsconfig( | ||||
|           null, 2)); | ||||
| } | ||||
| 
 | ||||
| export type TestableOptions = StrictTemplateOptions; | ||||
| 
 | ||||
| export class Project { | ||||
|   private tsProject: ts.server.Project; | ||||
| @ -52,7 +53,8 @@ export class Project { | ||||
|   private buffers = new Map<string, OpenBuffer>(); | ||||
| 
 | ||||
|   static initialize( | ||||
|       projectName: string, projectService: ts.server.ProjectService, files: ProjectFiles): Project { | ||||
|       projectName: string, projectService: ts.server.ProjectService, files: ProjectFiles, | ||||
|       options: TestableOptions = {}): Project { | ||||
|     const fs = getFileSystem(); | ||||
|     const tsConfigPath = absoluteFrom(`/${projectName}/tsconfig.json`); | ||||
| 
 | ||||
| @ -68,7 +70,7 @@ export class Project { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     writeTsconfig(fs, tsConfigPath, entryFiles, {}); | ||||
|     writeTsconfig(fs, tsConfigPath, entryFiles, options); | ||||
| 
 | ||||
|     // Ensure the project is live in the ProjectService.
 | ||||
|     projectService.openClientFile(entryFiles[0]); | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
| import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; | ||||
| import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; | ||||
| import {LanguageServiceTestEnv} from './env'; | ||||
| import {Project, ProjectFiles} from './project'; | ||||
| import {Project, ProjectFiles, TestableOptions} from './project'; | ||||
| 
 | ||||
| /** | ||||
|  * Given a text snippet which contains exactly one cursor symbol ('¦'), extract both the offset of | ||||
| @ -67,7 +67,7 @@ function getFirstClassDeclaration(declaration: string) { | ||||
| 
 | ||||
| export function createModuleAndProjectWithDeclarations( | ||||
|     env: LanguageServiceTestEnv, projectName: string, projectFiles: ProjectFiles, | ||||
|     options: any = {}): Project { | ||||
|     options: TestableOptions = {}): Project { | ||||
|   const externalClasses: string[] = []; | ||||
|   const externalImports: string[] = []; | ||||
|   for (const [fileName, fileContents] of Object.entries(projectFiles)) { | ||||
| @ -90,5 +90,28 @@ export function createModuleAndProjectWithDeclarations( | ||||
|         export class AppModule {} | ||||
|       `;
 | ||||
|   projectFiles['app-module.ts'] = moduleContents; | ||||
|   return env.addProject(projectName, projectFiles); | ||||
| } | ||||
|   return env.addProject(projectName, projectFiles, options); | ||||
| } | ||||
| 
 | ||||
| export function humanizeDocumentSpanLike<T extends ts.DocumentSpan>( | ||||
|     item: T, env: LanguageServiceTestEnv): T&Stringy<ts.DocumentSpan> { | ||||
|   return { | ||||
|     ...item, | ||||
|     textSpan: env.getTextFromTsSpan(item.fileName, item.textSpan), | ||||
|     contextSpan: item.contextSpan ? env.getTextFromTsSpan(item.fileName, item.contextSpan) : | ||||
|                                     undefined, | ||||
|     originalTextSpan: item.originalTextSpan ? | ||||
|         env.getTextFromTsSpan(item.fileName, item.originalTextSpan) : | ||||
|         undefined, | ||||
|     originalContextSpan: item.originalContextSpan ? | ||||
|         env.getTextFromTsSpan(item.fileName, item.originalContextSpan) : | ||||
|         undefined, | ||||
|   }; | ||||
| } | ||||
| type Stringy<T> = { | ||||
|   [P in keyof T]: string; | ||||
| }; | ||||
| 
 | ||||
| export function getText(contents: string, textSpan: ts.TextSpan) { | ||||
|   return contents.substr(textSpan.start, textSpan.length); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user