From b6893d23c52d5990d49308a0e87bbd36523e53fc Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 4 Nov 2020 12:28:59 -0800 Subject: [PATCH] test(language-service): introduce new virtual testing environment (#39594) This commit adds new language service testing infrastructure which allows for in-memory testing. It solves a number of issues with the previous testing infrastructure that relied on a single integration project across all of the tests, and also provides for much faster builds by using the compiler-cli's mock versions of @angular/core and @angular/common. A new `LanguageServiceTestEnvironment` class (conceptually mirroring the compiler-cli `NgtscTestEnvironment`) controls setup and execution of tests. The `FileSystem` abstraction is used to drive a `ts.server.ServerHost`, which backs the language service infrastructure. Since many language service tests revolve around the template, the API is currently optimized to spin up a "skeleton" project and then override its template for each test. The existing Quick Info tests (quick_info_spec.ts) were ported to the new infrastructure for validation. The tests were cleaned up a bit to remove unnecessary initializations as well as correct legitimate template errors which did not affect the test outcome, but caused additional validation of test correctness to fail. They still utilize a shared project with all fields required for each individual unit test, which is an anti-pattern, but new tests can now easily be written independently without relying on the shared project, which was extremely difficult previously. Future cleanup work might refactor these tests to be more independent. PR Close #39594 --- .../language-service/ivy/compiler_factory.ts | 41 ++-- .../language-service/ivy/language_service.ts | 12 +- .../language-service/ivy/test/BUILD.bazel | 31 +++ packages/language-service/ivy/test/env.ts | 219 ++++++++++++++++++ .../language-service/ivy/test/mock_host.ts | 121 ++++++++++ .../ivy/test/{legacy => }/quick_info_spec.ts | 170 ++++++++++---- 6 files changed, 528 insertions(+), 66 deletions(-) create mode 100644 packages/language-service/ivy/test/BUILD.bazel create mode 100644 packages/language-service/ivy/test/env.ts create mode 100644 packages/language-service/ivy/test/mock_host.ts rename packages/language-service/ivy/test/{legacy => }/quick_info_spec.ts (74%) diff --git a/packages/language-service/ivy/compiler_factory.ts b/packages/language-service/ivy/compiler_factory.ts index 8397044e7a..df0f26c243 100644 --- a/packages/language-service/ivy/compiler_factory.ts +++ b/packages/language-service/ivy/compiler_factory.ts @@ -15,6 +15,15 @@ import * as ts from 'typescript/lib/tsserverlibrary'; import {LanguageServiceAdapter} from './language_service_adapter'; import {isExternalTemplate} from './utils'; +/** + * Manages the `NgCompiler` instance which backs the language service, updating or replacing it as + * needed to produce an up-to-date understanding of the current program. + * + * TODO(alxhub): currently the options used for the compiler are specified at `CompilerFactory` + * construction, and are not changable. In a real project, users can update `tsconfig.json`. We need + * to properly handle a change in the compiler options, either by having an API to update the + * `CompilerFactory` to use new options, or by replacing it entirely. + */ export class CompilerFactory { private readonly incrementalStrategy = new TrackedIncrementalBuildStrategy(); private compiler: NgCompiler|null = null; @@ -23,21 +32,15 @@ export class CompilerFactory { constructor( private readonly adapter: LanguageServiceAdapter, private readonly programStrategy: TypeCheckingProgramStrategy, + private readonly options: NgCompilerOptions, ) {} - /** - * Create a new instance of the Ivy compiler if the program has changed since - * the last time the compiler was instantiated. If the program has not changed, - * return the existing instance. - * @param fileName override the template if this is an external template file - * @param options angular compiler options - */ - getOrCreateWithChangedFile(fileName: string, options: NgCompilerOptions): NgCompiler { + getOrCreate(): NgCompiler { const program = this.programStrategy.getProgram(); - if (!this.compiler || program !== this.lastKnownProgram) { + if (this.compiler === null || program !== this.lastKnownProgram) { this.compiler = new NgCompiler( this.adapter, // like compiler host - options, // angular compiler options + this.options, // angular compiler options program, this.programStrategy, this.incrementalStrategy, @@ -47,12 +50,24 @@ export class CompilerFactory { ); this.lastKnownProgram = program; } - if (isExternalTemplate(fileName)) { - this.overrideTemplate(fileName, this.compiler); - } return this.compiler; } + /** + * Create a new instance of the Ivy compiler if the program has changed since + * the last time the compiler was instantiated. If the program has not changed, + * return the existing instance. + * @param fileName override the template if this is an external template file + * @param options angular compiler options + */ + getOrCreateWithChangedFile(fileName: string): NgCompiler { + const compiler = this.getOrCreate(); + if (isExternalTemplate(fileName)) { + this.overrideTemplate(fileName, compiler); + } + return compiler; + } + private overrideTemplate(fileName: string, compiler: NgCompiler) { if (!this.adapter.isTemplateDirty(fileName)) { return; diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 4af86f7061..62d1e3dbb3 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -21,7 +21,7 @@ import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils'; export class LanguageService { private options: CompilerOptions; - private readonly compilerFactory: CompilerFactory; + readonly compilerFactory: CompilerFactory; private readonly strategy: TypeCheckingProgramStrategy; private readonly adapter: LanguageServiceAdapter; @@ -29,12 +29,12 @@ export class LanguageService { this.options = parseNgCompilerOptions(project); this.strategy = createTypeCheckingProgramStrategy(project); this.adapter = new LanguageServiceAdapter(project); - this.compilerFactory = new CompilerFactory(this.adapter, this.strategy); + this.compilerFactory = new CompilerFactory(this.adapter, this.strategy, this.options); this.watchConfigFile(project); } getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName, this.options); + const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); const ttc = compiler.getTemplateTypeChecker(); const diagnostics: ts.Diagnostic[] = []; if (isTypeScriptFile(fileName)) { @@ -57,7 +57,7 @@ export class LanguageService { getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan |undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName, this.options); + const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); const results = new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -66,7 +66,7 @@ export class LanguageService { getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[]|undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName, this.options); + const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); const results = new DefinitionBuilder(this.tsLS, compiler).getTypeDefinitionsAtPosition(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -75,7 +75,7 @@ export class LanguageService { getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined { const program = this.strategy.getProgram(); - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName, this.options); + const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); if (templateInfo === undefined) { return undefined; diff --git a/packages/language-service/ivy/test/BUILD.bazel b/packages/language-service/ivy/test/BUILD.bazel new file mode 100644 index 0000000000..5a23ac2fdf --- /dev/null +++ b/packages/language-service/ivy/test/BUILD.bazel @@ -0,0 +1,31 @@ +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") + +ts_library( + name = "test_lib", + testonly = True, + srcs = glob(["*.ts"]), + deps = [ + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/core:api", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/src/ngtsc/typecheck/api", + "//packages/language-service/ivy", + "@npm//typescript", + ], +) + +jasmine_node_test( + name = "test", + data = [ + "//packages/compiler-cli/src/ngtsc/testing/fake_common:npm_package", + "//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package", + ], + tags = [ + "ivy-only", + ], + deps = [ + ":test_lib", + ], +) diff --git a/packages/language-service/ivy/test/env.ts b/packages/language-service/ivy/test/env.ts new file mode 100644 index 0000000000..5351275162 --- /dev/null +++ b/packages/language-service/ivy/test/env.ts @@ -0,0 +1,219 @@ +/** + * @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 {TmplAstNode} from '@angular/compiler'; +import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; +import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {MockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; +import {TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import * as ts from 'typescript/lib/tsserverlibrary'; + +import {LanguageService} from '../language_service'; + +import {MockServerHost} from './mock_host'; + +function writeTsconfig( + fs: FileSystem, entryFiles: AbsoluteFsPath[], options: TestableOptions): void { + fs.writeFile( + absoluteFrom('/tsconfig.json'), + + JSON.stringify( + { + compilerOptions: { + strict: true, + experimentalDecorators: true, + moduleResolution: 'node', + target: 'es2015', + lib: [ + 'dom', + 'es2015', + ], + }, + files: entryFiles, + angularCompilerOptions: { + strictTemplates: true, + ...options, + } + }, + null, 2)); +} + +export type TestableOptions = StrictTemplateOptions; + +export class LanguageServiceTestEnvironment { + private constructor(private tsLS: ts.LanguageService, readonly ngLS: LanguageService) {} + + static setup(files: TestFile[], options: TestableOptions = {}): LanguageServiceTestEnvironment { + const fs = getFileSystem(); + if (!(fs instanceof MockFileSystem)) { + throw new Error(`LanguageServiceTestEnvironment only works with a mock filesystem`); + } + fs.init(loadStandardTestFiles({ + fakeCommon: true, + })); + + const host = new MockServerHost(fs); + const tsconfigPath = absoluteFrom('/tsconfig.json'); + + const entryFiles: AbsoluteFsPath[] = []; + for (const {name, contents, isRoot} of files) { + fs.writeFile(name, contents); + if (isRoot === true) { + entryFiles.push(name); + } + } + + if (entryFiles.length === 0) { + throw new Error(`Expected at least one root TestFile.`); + } + + writeTsconfig(fs, files.filter(file => file.isRoot === true).map(file => file.name), options); + + const projectService = new ts.server.ProjectService({ + host, + logger, + cancellationToken: ts.server.nullCancellationToken, + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true, + typingsInstaller: ts.server.nullTypingsInstaller, + }); + + // Open all root files. + for (const entryFile of entryFiles) { + projectService.openClientFile(entryFile); + } + + const project = projectService.findProject(tsconfigPath); + if (project === undefined) { + throw new Error(`Failed to create project for ${tsconfigPath}`); + } + // The following operation forces a ts.Program to be created. + const tsLS = project.getLanguageService(); + + const ngLS = new LanguageService(project, tsLS); + return new LanguageServiceTestEnvironment(tsLS, ngLS); + } + + getClass(fileName: AbsoluteFsPath, className: string): ts.ClassDeclaration { + const program = this.tsLS.getProgram(); + if (program === undefined) { + throw new Error(`Expected to get a ts.Program`); + } + const sf = getSourceFileOrError(program, fileName); + return getClassOrError(sf, className); + } + + overrideTemplateWithCursor(fileName: AbsoluteFsPath, className: string, contents: string): + {cursor: number, nodes: TmplAstNode[], component: ts.ClassDeclaration, text: string} { + const program = this.tsLS.getProgram(); + if (program === undefined) { + throw new Error(`Expected to get a ts.Program`); + } + const sf = getSourceFileOrError(program, fileName); + const component = getClassOrError(sf, className); + + const ngCompiler = this.ngLS.compilerFactory.getOrCreate(); + const templateTypeChecker = ngCompiler.getTemplateTypeChecker(); + + const {cursor, text} = extractCursorInfo(contents); + + const {nodes} = templateTypeChecker.overrideComponentTemplate(component, text); + return {cursor, nodes, component, text}; + } + + expectNoSourceDiagnostics(): void { + const program = this.tsLS.getProgram(); + if (program === undefined) { + throw new Error(`Expected to get a ts.Program`); + } + + const ngCompiler = this.ngLS.compilerFactory.getOrCreate(); + + for (const sf of program.getSourceFiles()) { + if (sf.isDeclarationFile || sf.fileName.endsWith('.ngtypecheck.ts')) { + continue; + } + + const syntactic = program.getSyntacticDiagnostics(sf); + expect(syntactic.map(diag => diag.messageText)).toEqual([]); + if (syntactic.length > 0) { + continue; + } + + const semantic = program.getSemanticDiagnostics(sf); + expect(semantic.map(diag => diag.messageText)).toEqual([]); + if (semantic.length > 0) { + continue; + } + + const ngDiagnostics = ngCompiler.getDiagnostics(sf); + expect(ngDiagnostics.map(diag => diag.messageText)).toEqual([]); + } + + this.ngLS.compilerFactory.registerLastKnownProgram(); + } + + expectNoTemplateDiagnostics(fileName: AbsoluteFsPath, className: string): void { + const program = this.tsLS.getProgram(); + if (program === undefined) { + throw new Error(`Expected to get a ts.Program`); + } + const sf = getSourceFileOrError(program, fileName); + const component = getClassOrError(sf, className); + + const diags = this.getTemplateTypeChecker().getDiagnosticsForComponent(component); + this.ngLS.compilerFactory.registerLastKnownProgram(); + expect(diags.map(diag => diag.messageText)).toEqual([]); + } + + getTemplateTypeChecker(): TemplateTypeChecker { + return this.ngLS.compilerFactory.getOrCreate().getTemplateTypeChecker(); + } +} + +const logger: ts.server.Logger = { + close(): void{}, + hasLevel(level: ts.server.LogLevel): boolean { + return false; + }, + loggingEnabled(): boolean { + return false; + }, + perftrc(s: string): void{}, + info(s: string): void{}, + startGroup(): void{}, + endGroup(): void{}, + msg(s: string, type?: ts.server.Msg): void{}, + getLogFileName(): string | + undefined { + return; + }, +}; + + +function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration { + for (const stmt of sf.statements) { + if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && stmt.name.text === name) { + return stmt; + } + } + throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`); +} + +function extractCursorInfo(textWithCursor: string): {cursor: number, text: string} { + const cursor = textWithCursor.indexOf('¦'); + if (cursor === -1) { + throw new Error(`Expected to find cursor symbol '¦'`); + } + + return { + cursor, + text: textWithCursor.substr(0, cursor) + textWithCursor.substr(cursor + 1), + }; +} diff --git a/packages/language-service/ivy/test/mock_host.ts b/packages/language-service/ivy/test/mock_host.ts new file mode 100644 index 0000000000..6f99cf1aa5 --- /dev/null +++ b/packages/language-service/ivy/test/mock_host.ts @@ -0,0 +1,121 @@ +/** + * @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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {MockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + +import * as ts from 'typescript/lib/tsserverlibrary'; + +const NOOP_FILE_WATCHER: ts.FileWatcher = { + close() {} +}; + +/** + * Adapts from the `ts.server.ServerHost` API to an in-memory filesystem. + */ +export class MockServerHost implements ts.server.ServerHost { + constructor(private fs: MockFileSystem) {} + + get newLine(): string { + return '\n'; + } + + get useCaseSensitiveFileNames(): boolean { + return this.fs.isCaseSensitive(); + } + + readFile(path: string, encoding?: string): string|undefined { + return this.fs.readFile(absoluteFrom(path)); + } + + resolvePath(path: string): string { + return this.fs.resolve(path); + } + + fileExists(path: string): boolean { + const absPath = absoluteFrom(path); + return this.fs.exists(absPath) && this.fs.lstat(absPath).isFile(); + } + + directoryExists(path: string): boolean { + const absPath = absoluteFrom(path); + return this.fs.exists(absPath) && this.fs.lstat(absPath).isDirectory(); + } + + createDirectory(path: string): void { + this.fs.ensureDir(absoluteFrom(path)); + } + + getExecutingFilePath(): string { + // This is load-bearing, as TypeScript uses the result of this call to locate the directory in + // which it expects to find .d.ts files for the "standard libraries" - DOM, ES2015, etc. + return '/node_modules/typescript/lib/tsserver.js'; + } + + getCurrentDirectory(): string { + return '/'; + } + + createHash(data: string): string { + return ts.sys.createHash!(data); + } + + get args(): string[] { + throw new Error('Property not implemented.'); + } + + watchFile( + path: string, callback: ts.FileWatcherCallback, pollingInterval?: number, + options?: ts.WatchOptions): ts.FileWatcher { + return NOOP_FILE_WATCHER; + } + + watchDirectory( + path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean, + options?: ts.WatchOptions): ts.FileWatcher { + return NOOP_FILE_WATCHER; + } + + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { + throw new Error('Method not implemented.'); + } + + clearTimeout(timeoutId: any): void { + throw new Error('Method not implemented.'); + } + + setImmediate(callback: (...args: any[]) => void, ...args: any[]) { + throw new Error('Method not implemented.'); + } + + clearImmediate(timeoutId: any): void { + throw new Error('Method not implemented.'); + } + + write(s: string): void { + throw new Error('Method not implemented.'); + } + + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void { + throw new Error('Method not implemented.'); + } + + getDirectories(path: string): string[] { + throw new Error('Method not implemented.'); + } + + readDirectory( + path: string, extensions?: readonly string[], exclude?: readonly string[], + include?: readonly string[], depth?: number): string[] { + throw new Error('Method not implemented.'); + } + + exit(exitCode?: number): void { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/language-service/ivy/test/legacy/quick_info_spec.ts b/packages/language-service/ivy/test/quick_info_spec.ts similarity index 74% rename from packages/language-service/ivy/test/legacy/quick_info_spec.ts rename to packages/language-service/ivy/test/quick_info_spec.ts index 8516c8a069..404b43531a 100644 --- a/packages/language-service/ivy/test/legacy/quick_info_spec.ts +++ b/packages/language-service/ivy/test/quick_info_spec.ts @@ -6,24 +6,107 @@ * found in the LICENSE file at https://angular.io/license */ +import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + import * as ts from 'typescript/lib/tsserverlibrary'; -import {LanguageService} from '../../language_service'; +import {LanguageServiceTestEnvironment} from './env'; -import {APP_COMPONENT, MockService, setup, TEST_TEMPLATE} from './mock_host'; +function quickInfoSkeleton(): TestFile[] { + return [ + { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component, Directive, EventEmitter, Input, NgModule, Output, Pipe, PipeTransform} from '@angular/core'; + import {CommonModule} from '@angular/common'; + + export interface Address { + streetName: string; + } + + /** The most heroic being. */ + export interface Hero { + id: number; + name: string; + address?: Address; + } + + /** + * This Component provides the \`test-comp\` selector. + */ + /*BeginTestComponent*/ @Component({ + selector: 'test-comp', + template: '
Testing: {{name}}
', + }) + export class TestComponent { + @Input('tcName') name!: string; + @Output('test') testEvent!: EventEmitter; + } /*EndTestComponent*/ + + @Component({ + selector: 'app-cmp', + templateUrl: './app.html', + }) + export class AppCmp { + hero!: Hero; + heroes!: Hero[]; + readonlyHeroes!: ReadonlyArray>; + /** + * This is the title of the \`AppCmp\` Component. + */ + title!: string; + constNames!: [{readonly name: 'name'}]; + birthday!: Date; + anyValue!: any; + myClick(event: any) {} + setTitle(newTitle: string) {} + trackByFn!: any; + name!: any; + } + + @Directive({ + selector: '[string-model]', + exportAs: 'stringModel', + }) + export class StringModel { + @Input() model!: string; + @Output() modelChange!: EventEmitter; + } + + @Directive({selector: 'button[custom-button][compound]'}) + export class CompoundCustomButtonDirective { + @Input() config?: {color?: string}; + } + + @NgModule({ + declarations: [ + AppCmp, + CompoundCustomButtonDirective, + StringModel, + TestComponent, + ], + imports: [ + CommonModule, + ], + }) + export class AppModule {} + `, + isRoot: true, + }, + { + name: absoluteFrom('/app.html'), + contents: `Will be overridden`, + } + ]; +} describe('quick info', () => { - let service: MockService; - let ngLS: LanguageService; - - beforeAll(() => { - const {project, service: _service, tsLS} = setup(); - service = _service; - ngLS = new LanguageService(project, tsLS); - }); + let env: LanguageServiceTestEnvironment; beforeEach(() => { - service.reset(); + initMockFileSystem('Native'); + env = LanguageServiceTestEnvironment.setup(quickInfoSkeleton()); }); describe('elements', () => { @@ -78,24 +161,23 @@ describe('quick info', () => { const {documentation} = expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'ngFor', - expectedDisplayString: '(directive) NgForOf>' + expectedDisplayString: '(directive) NgForOf' }); - expect(toText(documentation)) - .toContain('A [structural directive](guide/structural-directives) that renders'); + expect(toText(documentation)).toContain('A fake version of the NgFor directive.'); }); it('should work for directives with compound selectors, some of which are bindings', () => { expectQuickInfo({ - templateOverride: `{{item}}`, + templateOverride: `{{hero}}`, expectedSpanText: 'ngFor', - expectedDisplayString: '(directive) NgForOf>' + expectedDisplayString: '(directive) NgForOf' }); }); it('should work for data-let- syntax', () => { expectQuickInfo({ templateOverride: - `{{item}}`, + `{{hero}}`, expectedSpanText: 'hero', expectedDisplayString: '(variable) hero: Hero' }); @@ -127,7 +209,7 @@ describe('quick info', () => { it('should work for structural directive inputs ngForTrackBy', () => { expectQuickInfo({ - templateOverride: `
`, + templateOverride: `
`, expectedSpanText: 'trackBy', expectedDisplayString: '(property) NgForOf.ngForTrackBy: TrackByFunction' @@ -136,7 +218,7 @@ describe('quick info', () => { it('should work for structural directive inputs ngForOf', () => { expectQuickInfo({ - templateOverride: `
`, + templateOverride: `
`, expectedSpanText: 'of', expectedDisplayString: '(property) NgForOf.ngForOf: Hero[] | (Hero[] & Iterable) | null | undefined' @@ -157,7 +239,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', - expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' + expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); }); @@ -165,12 +247,12 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', - expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' + expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); expectQuickInfo({ templateOverride: ``, expectedSpanText: 'test', - expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' + expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter' }); }); @@ -272,7 +354,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: `
{{ tit¦le }}
`, expectedSpanText: 'title', - expectedDisplayString: '(property) AppComponent.title: string' + expectedDisplayString: '(property) AppCmp.title: string' }); }); @@ -288,7 +370,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'title', - expectedDisplayString: '(property) AppComponent.title: string' + expectedDisplayString: '(property) AppCmp.title: string' }); }); @@ -296,7 +378,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'title', - expectedDisplayString: '(property) AppComponent.title: string' + expectedDisplayString: '(property) AppCmp.title: string' }); }); @@ -312,7 +394,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: ``, expectedSpanText: 'title', - expectedDisplayString: '(property) AppComponent.title: string' + expectedDisplayString: '(property) AppCmp.title: string' }); }); @@ -320,7 +402,7 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'setTitle', - expectedDisplayString: '(method) AppComponent.setTitle(newTitle: string): void' + expectedDisplayString: '(method) AppCmp.setTitle(newTitle: string): void' }); }); @@ -342,9 +424,9 @@ describe('quick info', () => { it('should find members of two-way binding', () => { expectQuickInfo({ - templateOverride: ``, + templateOverride: ``, expectedSpanText: 'title', - expectedDisplayString: '(property) AppComponent.title: string' + expectedDisplayString: '(property) AppCmp.title: string' }); }); @@ -352,15 +434,15 @@ describe('quick info', () => { expectQuickInfo({ templateOverride: `
`, expectedSpanText: 'anyValue', - expectedDisplayString: '(property) AppComponent.anyValue: any' + expectedDisplayString: '(property) AppCmp.anyValue: any' }); }); it('should work for members in structural directives', () => { expectQuickInfo({ - templateOverride: `
`, + templateOverride: `
`, expectedSpanText: 'heroes', - expectedDisplayString: '(property) AppComponent.heroes: Hero[]' + expectedDisplayString: '(property) AppCmp.heroes: Hero[]' }); }); @@ -373,20 +455,11 @@ describe('quick info', () => { }); it('should provide documentation', () => { - const {position} = service.overwriteInlineTemplate(APP_COMPONENT, `
{{¦title}}
`); - const quickInfo = ngLS.getQuickInfoAtPosition(APP_COMPONENT, position); + const {cursor} = env.overrideTemplateWithCursor( + absoluteFrom('/app.ts'), 'AppCmp', `
{{¦title}}
`); + const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); const documentation = toText(quickInfo!.documentation); - expect(documentation).toBe('This is the title of the `AppComponent` Component.'); - }); - - it('works with external template', () => { - const {position, text} = service.overwrite(TEST_TEMPLATE, ''); - const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, position); - expect(quickInfo).toBeTruthy(); - const {textSpan, displayParts} = quickInfo!; - expect(text.substring(textSpan.start, textSpan.start + textSpan.length)) - .toEqual(''); - expect(toText(displayParts)).toEqual('(element) button: HTMLButtonElement'); + expect(documentation).toBe('This is the title of the `AppCmp` Component.'); }); }); @@ -394,8 +467,11 @@ describe('quick info', () => { {templateOverride, expectedSpanText, expectedDisplayString}: {templateOverride: string, expectedSpanText: string, expectedDisplayString: string}): ts.QuickInfo { - const {position, text} = service.overwriteInlineTemplate(APP_COMPONENT, templateOverride); - const quickInfo = ngLS.getQuickInfoAtPosition(APP_COMPONENT, position); + const {cursor, text} = + env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride); + env.expectNoSourceDiagnostics(); + env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp'); + const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo!; expect(text.substring(textSpan.start, textSpan.start + textSpan.length))