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))