From af3f95bd755f56b8d4dda8373c56a6109b813b9e Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 8 Feb 2021 09:54:28 -0800 Subject: [PATCH] refactor(language-service): migrate diagnostic_spec to new test infrastructure (#40966) refactor(language-service): migrate diagnostic_spec to new test infrastructure PR Close #40966 --- .../ivy/test/diagnostic_spec.ts | 121 ++++++++---------- .../language-service/ivy/testing/src/util.ts | 40 ++++++ 2 files changed, 91 insertions(+), 70 deletions(-) diff --git a/packages/language-service/ivy/test/diagnostic_spec.ts b/packages/language-service/ivy/test/diagnostic_spec.ts index ea37503a09..97ccf9dc1d 100644 --- a/packages/language-service/ivy/test/diagnostic_spec.ts +++ b/packages/language-service/ivy/test/diagnostic_spec.ts @@ -6,21 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; -import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env'; import * as ts from 'typescript'; -import {createModuleWithDeclarations} from './test_utils'; + +import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv} from '../testing'; + describe('getSemanticDiagnostics', () => { + let env: LanguageServiceTestEnv; beforeEach(() => { initMockFileSystem('Native'); + env = LanguageServiceTestEnv.setup(); }); it('should not produce error for a minimal component defintion', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -29,16 +30,15 @@ describe('getSemanticDiagnostics', () => { export class AppComponent {} ` }; - const env = createModuleWithDeclarations([appFile]); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); + const diags = project.getDiagnosticsForFile('app.ts'); expect(diags.length).toEqual(0); }); it('should report member does not exist', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -47,67 +47,59 @@ describe('getSemanticDiagnostics', () => { export class AppComponent {} ` }; - const env = createModuleWithDeclarations([appFile]); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); + const diags = project.getDiagnosticsForFile('app.ts'); expect(diags.length).toBe(1); - const {category, file, start, length, messageText} = diags[0]; + const {category, file, messageText} = diags[0]; expect(category).toBe(ts.DiagnosticCategory.Error); - expect(file?.fileName).toBe('/app.ts'); + expect(file?.fileName).toBe('/test/app.ts'); expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); }); it('should process external template', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ templateUrl: './app.html' }) export class AppComponent {} - ` - }; - const templateFile = { - name: absoluteFrom('/app.html'), - contents: ` - Hello world! - ` + `, + 'app.html': `Hello world!` }; - const env = createModuleWithDeclarations([appFile], [templateFile]); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const diags = project.getDiagnosticsForFile('app.html'); expect(diags).toEqual([]); }); it('should report member does not exist in external template', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ templateUrl: './app.html' }) export class AppComponent {} - ` + `, + 'app.html': '{{nope}}' }; - const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`}; - const env = createModuleWithDeclarations([appFile], [templateFile]); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const diags = project.getDiagnosticsForFile('app.html'); expect(diags.length).toBe(1); - const {category, file, start, length, messageText} = diags[0]; + const {category, file, messageText} = diags[0]; expect(category).toBe(ts.DiagnosticCategory.Error); - expect(file?.fileName).toBe('/app.html'); + expect(file?.fileName).toBe('/test/app.html'); expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); }); it('should report a parse error in external template', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -116,26 +108,25 @@ describe('getSemanticDiagnostics', () => { export class AppComponent { nope = false; } - ` + `, + 'app.html': '{{nope = true}}' }; - const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope = true}}`}; - const env = createModuleWithDeclarations([appFile], [templateFile]); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const diags = project.getDiagnosticsForFile('app.html'); expect(diags.length).toBe(1); const {category, file, messageText} = diags[0]; expect(category).toBe(ts.DiagnosticCategory.Error); - expect(file?.fileName).toBe('/app.html'); + expect(file?.fileName).toBe('/test/app.html'); expect(messageText) .toContain( `Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}]`); }); it('should report parse errors of components defined in the same ts file', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ templateUrl: './app1.html' }) @@ -143,14 +134,10 @@ describe('getSemanticDiagnostics', () => { @Component({ templateUrl: './app2.html' }) export class AppComponent2 { nope = false; } - ` - }; - const templateFile1 = {name: absoluteFrom('/app1.html'), contents: `{{nope = false}}`}; - const templateFile2 = {name: absoluteFrom('/app2.html'), contents: `{{nope = true}}`}; - - const moduleFile = { - name: absoluteFrom('/app-module.ts'), - contents: ` + `, + 'app1.html': '{{nope = false}}', + 'app2.html': '{{nope = true}}', + 'app-module.ts': ` import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {AppComponent, AppComponent2} from './app'; @@ -160,34 +147,28 @@ describe('getSemanticDiagnostics', () => { imports: [CommonModule], }) export class AppModule {} - `, - isRoot: true + ` }; - const env = - LanguageServiceTestEnvironment.setup([moduleFile, appFile, templateFile1, templateFile2]); - - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); - + const project = env.addProject('test', files); + const diags = project.getDiagnosticsForFile('app.ts'); expect(diags.map(x => x.messageText).sort()).toEqual([ - 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /app1.html@0:0', - 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /app2.html@0:0' + 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /test/app1.html@0:0', + 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /test/app2.html@0:0' ]); }); it('reports a diagnostic for a component without a template', () => { - const appFile = { - name: absoluteFrom('/app.ts'), - contents: ` + const files = { + 'app.ts': ` import {Component} from '@angular/core'; @Component({}) export class MyComponent {} ` }; - const env = createModuleWithDeclarations([appFile]); - const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); - + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const diags = project.getDiagnosticsForFile('app.ts'); expect(diags.map(x => x.messageText)).toEqual([ 'component is missing a template', ]); diff --git a/packages/language-service/ivy/testing/src/util.ts b/packages/language-service/ivy/testing/src/util.ts index 8b1bd99cd2..9e13b9ded1 100644 --- a/packages/language-service/ivy/testing/src/util.ts +++ b/packages/language-service/ivy/testing/src/util.ts @@ -5,6 +5,10 @@ * 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 {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {LanguageServiceTestEnv} from './env'; +import {Project, ProjectFiles} from './project'; /** * Given a text snippet which contains exactly one cursor symbol ('¦'), extract both the offset of @@ -52,3 +56,39 @@ export function isNgSpecificDiagnostic(diag: ts.Diagnostic): boolean { // Angular-specific diagnostics use a negative code space. return diag.code < 0; } + +function getFirstClassDeclaration(declaration: string) { + const matches = declaration.match(/(?:export class )(\w+)(?:\s|\{)/); + if (matches === null || matches.length !== 2) { + throw new Error(`Did not find exactly one exported class in: ${declaration}`); + } + return matches[1].trim(); +} + +export function createModuleAndProjectWithDeclarations( + env: LanguageServiceTestEnv, projectName: string, projectFiles: ProjectFiles, + options: any = {}): Project { + const externalClasses: string[] = []; + const externalImports: string[] = []; + for (const [fileName, fileContents] of Object.entries(projectFiles)) { + if (!fileName.endsWith('.ts')) { + continue; + } + const className = getFirstClassDeclaration(fileContents); + externalClasses.push(className); + externalImports.push(`import {${className}} from './${fileName.replace('.ts', '')}';`); + } + const moduleContents = ` + import {NgModule} from '@angular/core'; + import {CommonModule} from '@angular/common'; + ${externalImports.join('\n')} + + @NgModule({ + declarations: [${externalClasses.join(',')}], + imports: [CommonModule], + }) + export class AppModule {} + `; + projectFiles['app-module.ts'] = moduleContents; + return env.addProject(projectName, projectFiles); +} \ No newline at end of file