refactor(language-service): migrate diagnostic_spec to new test infrastructure (#40966)
refactor(language-service): migrate diagnostic_spec to new test infrastructure PR Close #40966
This commit is contained in:
parent
bc5c9ee234
commit
af3f95bd75
@ -6,21 +6,22 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {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 * as ts from 'typescript';
|
||||||
import {createModuleWithDeclarations} from './test_utils';
|
|
||||||
|
import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv} from '../testing';
|
||||||
|
|
||||||
|
|
||||||
describe('getSemanticDiagnostics', () => {
|
describe('getSemanticDiagnostics', () => {
|
||||||
|
let env: LanguageServiceTestEnv;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initMockFileSystem('Native');
|
initMockFileSystem('Native');
|
||||||
|
env = LanguageServiceTestEnv.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not produce error for a minimal component defintion', () => {
|
it('should not produce error for a minimal component defintion', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -29,16 +30,15 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
export class AppComponent {}
|
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);
|
expect(diags.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report member does not exist', () => {
|
it('should report member does not exist', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -47,67 +47,59 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
export class AppComponent {}
|
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);
|
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(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'.`);
|
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process external template', () => {
|
it('should process external template', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './app.html'
|
templateUrl: './app.html'
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
`
|
`,
|
||||||
};
|
'app.html': `Hello world!`
|
||||||
const templateFile = {
|
|
||||||
name: absoluteFrom('/app.html'),
|
|
||||||
contents: `
|
|
||||||
Hello world!
|
|
||||||
`
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const env = createModuleWithDeclarations([appFile], [templateFile]);
|
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
|
||||||
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
|
const diags = project.getDiagnosticsForFile('app.html');
|
||||||
expect(diags).toEqual([]);
|
expect(diags).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report member does not exist in external template', () => {
|
it('should report member does not exist in external template', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './app.html'
|
templateUrl: './app.html'
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
`
|
`,
|
||||||
|
'app.html': '{{nope}}'
|
||||||
};
|
};
|
||||||
const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`};
|
|
||||||
|
|
||||||
const env = createModuleWithDeclarations([appFile], [templateFile]);
|
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
|
||||||
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
|
const diags = project.getDiagnosticsForFile('app.html');
|
||||||
expect(diags.length).toBe(1);
|
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(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'.`);
|
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report a parse error in external template', () => {
|
it('should report a parse error in external template', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -116,26 +108,25 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
nope = false;
|
nope = false;
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
|
'app.html': '{{nope = true}}'
|
||||||
};
|
};
|
||||||
const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope = true}}`};
|
|
||||||
|
|
||||||
const env = createModuleWithDeclarations([appFile], [templateFile]);
|
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
|
||||||
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
|
const diags = project.getDiagnosticsForFile('app.html');
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
|
|
||||||
const {category, file, messageText} = diags[0];
|
const {category, file, messageText} = diags[0];
|
||||||
expect(category).toBe(ts.DiagnosticCategory.Error);
|
expect(category).toBe(ts.DiagnosticCategory.Error);
|
||||||
expect(file?.fileName).toBe('/app.html');
|
expect(file?.fileName).toBe('/test/app.html');
|
||||||
expect(messageText)
|
expect(messageText)
|
||||||
.toContain(
|
.toContain(
|
||||||
`Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}]`);
|
`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', () => {
|
it('should report parse errors of components defined in the same ts file', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({ templateUrl: './app1.html' })
|
@Component({ templateUrl: './app1.html' })
|
||||||
@ -143,14 +134,10 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
|
|
||||||
@Component({ templateUrl: './app2.html' })
|
@Component({ templateUrl: './app2.html' })
|
||||||
export class AppComponent2 { nope = false; }
|
export class AppComponent2 { nope = false; }
|
||||||
`
|
`,
|
||||||
};
|
'app1.html': '{{nope = false}}',
|
||||||
const templateFile1 = {name: absoluteFrom('/app1.html'), contents: `{{nope = false}}`};
|
'app2.html': '{{nope = true}}',
|
||||||
const templateFile2 = {name: absoluteFrom('/app2.html'), contents: `{{nope = true}}`};
|
'app-module.ts': `
|
||||||
|
|
||||||
const moduleFile = {
|
|
||||||
name: absoluteFrom('/app-module.ts'),
|
|
||||||
contents: `
|
|
||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {AppComponent, AppComponent2} from './app';
|
import {AppComponent, AppComponent2} from './app';
|
||||||
@ -160,34 +147,28 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
`,
|
`
|
||||||
isRoot: true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const env =
|
const project = env.addProject('test', files);
|
||||||
LanguageServiceTestEnvironment.setup([moduleFile, appFile, templateFile1, templateFile2]);
|
const diags = project.getDiagnosticsForFile('app.ts');
|
||||||
|
|
||||||
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
|
|
||||||
|
|
||||||
expect(diags.map(x => x.messageText).sort()).toEqual([
|
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 = false}}] in /test/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 = true}}] in /test/app2.html@0:0'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports a diagnostic for a component without a template', () => {
|
it('reports a diagnostic for a component without a template', () => {
|
||||||
const appFile = {
|
const files = {
|
||||||
name: absoluteFrom('/app.ts'),
|
'app.ts': `
|
||||||
contents: `
|
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
@Component({})
|
@Component({})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
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.map(x => x.messageText)).toEqual([
|
expect(diags.map(x => x.messageText)).toEqual([
|
||||||
'component is missing a template',
|
'component is missing a template',
|
||||||
]);
|
]);
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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
|
* 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.
|
// Angular-specific diagnostics use a negative code space.
|
||||||
return diag.code < 0;
|
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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user