From d2b43d577b7163a0331c27f57a4ade68a37593fa Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 8 Feb 2021 10:43:21 -0800 Subject: [PATCH] refactor(language-service): migrate definitions_spec to new testing package (#40966) refactor(language-service): migrate definitions_spec to new testing package PR Close #40966 --- .../ivy/test/definitions_spec.ts | 128 ++++++++---------- .../language-service/ivy/testing/src/env.ts | 12 +- .../ivy/testing/src/project.ts | 8 +- .../language-service/ivy/testing/src/util.ts | 31 ++++- 4 files changed, 101 insertions(+), 78 deletions(-) diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index 056bf1b2a9..db341d9ab7 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -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'), ' {{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 = ' {{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('
'); - 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': '
', + '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('
'); - const templateFile = {contents: text, name: absoluteFrom('/app.html')}; - const dirFile = { - name: absoluteFrom('/dir.ts'), - contents: ` + const files = { + 'app.html': '
', + 'dir.ts': ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({selector: '[dir]'}) export class MyDir { @Output() someEvent = new EventEmitter(); }`, - }; - 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(); }`, - }; - 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))}; diff --git a/packages/language-service/ivy/testing/src/env.ts b/packages/language-service/ivy/testing/src/env.ts index 1a69f3c302..482c81334c 100644 --- a/packages/language-service/ivy/testing/src/env.ts +++ b/packages/language-service/ivy/testing/src/env.ts @@ -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 = { diff --git a/packages/language-service/ivy/testing/src/project.ts b/packages/language-service/ivy/testing/src/project.ts index 6c5dfccf06..e59805bba2 100644 --- a/packages/language-service/ivy/testing/src/project.ts +++ b/packages/language-service/ivy/testing/src/project.ts @@ -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(); 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]); diff --git a/packages/language-service/ivy/testing/src/util.ts b/packages/language-service/ivy/testing/src/util.ts index 9e13b9ded1..50208c58f5 100644 --- a/packages/language-service/ivy/testing/src/util.ts +++ b/packages/language-service/ivy/testing/src/util.ts @@ -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); -} \ No newline at end of file + return env.addProject(projectName, projectFiles, options); +} + +export function humanizeDocumentSpanLike( + item: T, env: LanguageServiceTestEnv): T&Stringy { + 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 = { + [P in keyof T]: string; +}; + +export function getText(contents: string, textSpan: ts.TextSpan) { + return contents.substr(textSpan.start, textSpan.length); +}