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:
Andrew Scott 2021-02-08 10:43:21 -08:00 committed by atscott
parent 8808002e54
commit d2b43d577b
4 changed files with 101 additions and 78 deletions

View File

@ -6,102 +6,100 @@
* found in the LICENSE file at https://angular.io/license * 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 {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; import {assertFileNames, createModuleAndProjectWithDeclarations, humanizeDocumentSpanLike, LanguageServiceTestEnv, OpenBuffer} from '../testing';
import {assertFileNames, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils';
describe('definitions', () => { describe('definitions', () => {
it('gets definition for template reference in overridden template', () => { it('gets definition for template reference in overridden template', () => {
initMockFileSystem('Native'); initMockFileSystem('Native');
const templateFile = {contents: '', name: absoluteFrom('/app.html')}; const files = {
const appFile = { 'app.html': '',
name: absoluteFrom('/app.ts'), 'app.ts': `
contents: `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({templateUrl: '/app.html'}) @Component({templateUrl: '/app.html'})
export class AppCmp {} export class AppCmp {}
`, `,
}; };
const env = LanguageServiceTestEnv.setup();
const env = createModuleWithDeclarations([appFile], [templateFile]); const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const {cursor} = env.updateFileWithCursor( const template = project.openFile('app.html');
absoluteFrom('/app.html'), '<input #myInput /> {{myIn¦put.value}}'); template.contents = '<input #myInput /> {{myInput.value}}';
env.expectNoSourceDiagnostics(); project.expectNoSourceDiagnostics();
const {definitions} = env.ngLS.getDefinitionAndBoundSpan(absoluteFrom('/app.html'), cursor)!;
template.moveCursorToText('{{myIn¦put.value}}');
const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
expect(definitions![0].name).toEqual('myInput'); expect(definitions![0].name).toEqual('myInput');
assertFileNames(Array.from(definitions!), ['app.html']); assertFileNames(Array.from(definitions!), ['app.html']);
}); });
it('returns the pipe class as definition when checkTypeOfPipes is false', () => { it('returns the pipe class as definition when checkTypeOfPipes is false', () => {
initMockFileSystem('Native'); initMockFileSystem('Native');
const {cursor, text} = extractCursorInfo('{{"1/1/2020" | dat¦e}}'); const files = {
const templateFile = {contents: text, name: absoluteFrom('/app.html')}; 'app.ts': `
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
@Component({templateUrl: 'app.html'}) @Component({templateUrl: 'app.html'})
export class AppCmp {} export class AppCmp {}
`, `,
'app.html': '{{"1/1/2020" | date}}'
}; };
// checkTypeOfPipes is set to false when strict templates is false // checkTypeOfPipes is set to false when strict templates is false
const env = createModuleWithDeclarations([appFile], [templateFile], {strictTemplates: false}); const env = LanguageServiceTestEnv.setup();
const {textSpan, definitions} = const project =
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); createModuleAndProjectWithDeclarations(env, 'test', files, {strictTemplates: false});
expect(text.substr(textSpan.start, textSpan.length)).toEqual('date'); const template = project.openFile('app.html');
project.expectNoSourceDiagnostics();
template.moveCursorToText('da¦te');
expect(definitions.length).toEqual(1); const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
const [def] = definitions; 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.textSpan).toContain('DatePipe');
expect(def.contextSpan).toContain('DatePipe'); expect(def.contextSpan).toContain('DatePipe');
}); });
it('gets definitions for all inputs when attribute matches more than one', () => { it('gets definitions for all inputs when attribute matches more than one', () => {
initMockFileSystem('Native'); initMockFileSystem('Native');
const {cursor, text} = extractCursorInfo('<div dir inpu¦tA="abc"></div>'); const files = {
const templateFile = {contents: text, name: absoluteFrom('/app.html')}; 'app.ts': `
const dirFile = { import {Component, NgModule} from '@angular/core';
name: absoluteFrom('/dir.ts'), import {CommonModule} from '@angular/common';
contents: `
@Component({templateUrl: 'app.html'})
export class AppCmp {}
`,
'app.html': '<div dir inputA="abc"></div>',
'dir.ts': `
import {Directive, Input} from '@angular/core'; import {Directive, Input} from '@angular/core';
@Directive({selector: '[dir]'}) @Directive({selector: '[dir]'})
export class MyDir { export class MyDir {
@Input() inputA!: any; @Input() inputA!: any;
}`, }`,
}; 'dir2.ts': `
const dirFile2 = {
name: absoluteFrom('/dir2.ts'),
contents: `
import {Directive, Input} from '@angular/core'; import {Directive, Input} from '@angular/core';
@Directive({selector: '[dir]'}) @Directive({selector: '[dir]'})
export class MyDir2 { export class MyDir2 {
@Input() inputA!: any; @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 env = LanguageServiceTestEnv.setup();
const {textSpan, definitions} = const project = createModuleAndProjectWithDeclarations(env, 'test', files);
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); const template = project.openFile('app.html');
expect(text.substr(textSpan.start, textSpan.length)).toEqual('inputA'); template.moveCursorToText('inpu¦tA="abc"');
expect(definitions.length).toEqual(2); const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
const [def, def2] = definitions; 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(def.textSpan).toContain('inputA');
expect(def2.textSpan).toContain('inputA'); expect(def2.textSpan).toContain('inputA');
// TODO(atscott): investigate why the text span includes more than just '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', () => { it('gets definitions for all outputs when attribute matches more than one', () => {
initMockFileSystem('Native'); initMockFileSystem('Native');
const {cursor, text} = extractCursorInfo('<div dir (someEv¦ent)="doSomething()"></div>'); const files = {
const templateFile = {contents: text, name: absoluteFrom('/app.html')}; 'app.html': '<div dir (someEvent)="doSomething()"></div>',
const dirFile = { 'dir.ts': `
name: absoluteFrom('/dir.ts'),
contents: `
import {Directive, Output, EventEmitter} from '@angular/core'; import {Directive, Output, EventEmitter} from '@angular/core';
@Directive({selector: '[dir]'}) @Directive({selector: '[dir]'})
export class MyDir { export class MyDir {
@Output() someEvent = new EventEmitter<void>(); @Output() someEvent = new EventEmitter<void>();
}`, }`,
}; 'dir2.ts': `
const dirFile2 = {
name: absoluteFrom('/dir2.ts'),
contents: `
import {Directive, Output, EventEmitter} from '@angular/core'; import {Directive, Output, EventEmitter} from '@angular/core';
@Directive({selector: '[dir]'}) @Directive({selector: '[dir]'})
export class MyDir2 { export class MyDir2 {
@Output() someEvent = new EventEmitter<void>(); @Output() someEvent = new EventEmitter<void>();
}`, }`,
}; 'app.ts': `
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
@ -145,10 +135,13 @@ describe('definitions', () => {
} }
` `
}; };
const env = createModuleWithDeclarations([appFile, dirFile, dirFile2], [templateFile]); const env = LanguageServiceTestEnv.setup();
const {textSpan, definitions} = const project = createModuleAndProjectWithDeclarations(env, 'test', files);
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor); const template = project.openFile('app.html');
expect(text.substr(textSpan.start, textSpan.length)).toEqual('someEvent'); 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); expect(definitions.length).toEqual(2);
const [def, def2] = definitions; const [def, def2] = definitions;
@ -159,10 +152,9 @@ describe('definitions', () => {
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']); assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
}); });
function getDefinitionsAndAssertBoundSpan( function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) {
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
env.expectNoSourceDiagnostics(); env.expectNoSourceDiagnostics();
const definitionAndBoundSpan = env.ngLS.getDefinitionAndBoundSpan(fileName, cursor); const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();
const {textSpan, definitions} = definitionAndBoundSpan!; const {textSpan, definitions} = definitionAndBoundSpan!;
expect(definitions).toBeTruthy(); expect(definitions).toBeTruthy();
return {textSpan, definitions: definitions!.map(d => humanizeDocumentSpanLike(d, env))}; return {textSpan, definitions: definitions!.map(d => humanizeDocumentSpanLike(d, env))};

View File

@ -12,7 +12,7 @@ import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
import * as ts from 'typescript/lib/tsserverlibrary'; import * as ts from 'typescript/lib/tsserverlibrary';
import {MockServerHost} from './host'; 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 * 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) {} 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)) { if (this.projects.has(name)) {
throw new Error(`Project ${name} is already defined`); 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); this.projects.set(name, project);
return project; return project;
} }
@ -65,6 +65,12 @@ export class LanguageServiceTestEnv {
} }
return scriptInfo.getSnapshot().getText(span.start, span.start + span.length); 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 = { const logger: ts.server.Logger = {

View File

@ -8,7 +8,7 @@
import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; 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 * as ts from 'typescript/lib/tsserverlibrary';
import {LanguageService} from '../../language_service'; import {LanguageService} from '../../language_service';
import {OpenBuffer} from './buffer'; import {OpenBuffer} from './buffer';
@ -44,6 +44,7 @@ function writeTsconfig(
null, 2)); null, 2));
} }
export type TestableOptions = StrictTemplateOptions;
export class Project { export class Project {
private tsProject: ts.server.Project; private tsProject: ts.server.Project;
@ -52,7 +53,8 @@ export class Project {
private buffers = new Map<string, OpenBuffer>(); private buffers = new Map<string, OpenBuffer>();
static initialize( 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 fs = getFileSystem();
const tsConfigPath = absoluteFrom(`/${projectName}/tsconfig.json`); 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. // Ensure the project is live in the ProjectService.
projectService.openClientFile(entryFiles[0]); projectService.openClientFile(entryFiles[0]);

View File

@ -8,7 +8,7 @@
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {LanguageServiceTestEnv} from './env'; 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 * 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( export function createModuleAndProjectWithDeclarations(
env: LanguageServiceTestEnv, projectName: string, projectFiles: ProjectFiles, env: LanguageServiceTestEnv, projectName: string, projectFiles: ProjectFiles,
options: any = {}): Project { options: TestableOptions = {}): Project {
const externalClasses: string[] = []; const externalClasses: string[] = [];
const externalImports: string[] = []; const externalImports: string[] = [];
for (const [fileName, fileContents] of Object.entries(projectFiles)) { for (const [fileName, fileContents] of Object.entries(projectFiles)) {
@ -90,5 +90,28 @@ export function createModuleAndProjectWithDeclarations(
export class AppModule {} export class AppModule {}
`; `;
projectFiles['app-module.ts'] = moduleContents; 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);
} }