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
|
* 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))};
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
Loading…
Reference in New Issue