test(language-service): convert ivy diagnostics tests from legacy (#39957)

This commit updates the tests for the diagnostics in the ivy language
service to use the new in-memory test environment.

PR Close #39957
This commit is contained in:
Zach Arend 2020-12-03 11:42:46 -08:00 committed by Misko Hevery
parent 65651b094d
commit de8f0fe5ee
5 changed files with 216 additions and 134 deletions

View File

@ -0,0 +1,108 @@
/**
* @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 {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';
describe('getSemanticDiagnostics', () => {
let env: LanguageServiceTestEnvironment;
beforeEach(() => {
initMockFileSystem('Native');
});
it('should not produce error for a minimal component defintion', () => {
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
@Component({
template: ''
})
export class AppComponent {}
`
};
env = createModuleWithDeclarations([appFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
expect(diags.length).toEqual(0);
});
it('should report member does not exist', () => {
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
@Component({
template: '{{nope}}'
})
export class AppComponent {}
`
};
env = createModuleWithDeclarations([appFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
expect(diags.length).toBe(1);
const {category, file, start, length, messageText} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe('/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: `
import {Component, NgModule} from '@angular/core';
@Component({
templateUrl: './app.html'
})
export class AppComponent {}
`
};
const templateFile = {
name: absoluteFrom('/app.html'),
contents: `
Hello world!
`
};
env = createModuleWithDeclarations([appFile], [templateFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
expect(diags).toEqual([]);
});
it('should report member does not exist in external template', () => {
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
@Component({
templateUrl: './app.html'
})
export class AppComponent {}
`
};
const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`};
env = createModuleWithDeclarations([appFile], [templateFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
expect(diags.length).toBe(1);
const {category, file, start, length, messageText} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe('/app.html');
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
});
});

View File

@ -0,0 +1,41 @@
/**
* @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 {LanguageService} from '../../language_service';
import {MockService, setup, TEST_TEMPLATE} from './mock_host';
describe('getSemanticDiagnostics', () => {
let service: MockService;
let ngLS: LanguageService;
beforeAll(() => {
const {project, service: _service, tsLS} = setup();
service = _service;
ngLS = new LanguageService(project, tsLS);
});
beforeEach(() => {
service.reset();
});
it('should retrieve external template from latest snapshot', () => {
// This test is to make sure we are reading from snapshot instead of disk
// if content from snapshot is newer. It also makes sure the internal cache
// of the resource loader is invalidated on content change.
service.overwrite(TEST_TEMPLATE, `{{ foo }}`);
const d1 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(d1.length).toBe(1);
expect(d1[0].messageText).toBe(`Property 'foo' does not exist on type 'TemplateReference'.`);
service.overwrite(TEST_TEMPLATE, `{{ bar }}`);
const d2 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(d2.length).toBe(1);
expect(d2[0].messageText).toBe(`Property 'bar' does not exist on type 'TemplateReference'.`);
});
});

View File

@ -1,75 +0,0 @@
/**
* @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 * as ts from 'typescript/lib/tsserverlibrary';
import {LanguageService} from '../../language_service';
import {APP_COMPONENT, MockService, setup, TEST_TEMPLATE} from './mock_host';
describe('getSemanticDiagnostics', () => {
let service: MockService;
let ngLS: LanguageService;
beforeAll(() => {
const {project, service: _service, tsLS} = setup();
service = _service;
ngLS = new LanguageService(project, tsLS);
});
beforeEach(() => {
service.reset();
});
it('should not produce error for AppComponent', () => {
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
expect(diags).toEqual([]);
});
it('should report member does not exist', () => {
const {text} = service.overwriteInlineTemplate(APP_COMPONENT, '{{ nope }}');
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
expect(diags.length).toBe(1);
const {category, file, start, length, messageText} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe(APP_COMPONENT);
expect(text.substring(start!, start! + length!)).toBe('nope');
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
});
it('should process external template', () => {
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags).toEqual([]);
});
it('should report member does not exist in external template', () => {
const {text} = service.overwrite(TEST_TEMPLATE, `{{ nope }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {category, file, start, length, messageText} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe(TEST_TEMPLATE);
expect(text.substring(start!, start! + length!)).toBe('nope');
expect(messageText).toBe(`Property 'nope' does not exist on type 'TemplateReference'.`);
});
it('should retrieve external template from latest snapshot', () => {
// This test is to make sure we are reading from snapshot instead of disk
// if content from snapshot is newer. It also makes sure the internal cache
// of the resource loader is invalidated on content change.
service.overwrite(TEST_TEMPLATE, `{{ foo }}`);
const d1 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(d1.length).toBe(1);
expect(d1[0].messageText).toBe(`Property 'foo' does not exist on type 'TemplateReference'.`);
service.overwrite(TEST_TEMPLATE, `{{ bar }}`);
const d2 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(d2.length).toBe(1);
expect(d2[0].messageText).toBe(`Property 'bar' does not exist on type 'TemplateReference'.`);
});
});

View File

@ -11,7 +11,7 @@ import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file
import * as ts from 'typescript/lib/tsserverlibrary';
import {extractCursorInfo, LanguageServiceTestEnvironment} from './env';
import {getText} from './test_utils';
import {createModuleWithDeclarations, getText} from './test_utils';
describe('find references', () => {
let env: LanguageServiceTestEnvironment;
@ -30,7 +30,7 @@ describe('find references', () => {
}`);
const appFile = {name: _('/app.ts'), contents: text};
const templateFile = {name: _('/app.html'), contents: '{{myProp}}'};
createModuleWithDeclarations([appFile], [templateFile]);
env = createModuleWithDeclarations([appFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.html', 'app.ts']);
@ -46,7 +46,7 @@ describe('find references', () => {
myP¦rop!: string;
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.ts']);
@ -66,7 +66,7 @@ describe('find references', () => {
};
const {text, cursor} = extractCursorInfo('{{myP¦rop}}');
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile], [templateFile]);
env = createModuleWithDeclarations([appFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.html', 'app.ts']);
@ -82,7 +82,7 @@ describe('find references', () => {
setTitle(s: number) {}
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
@ -100,7 +100,7 @@ describe('find references', () => {
setTitle(s: string) {}
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
@ -121,7 +121,7 @@ describe('find references', () => {
const templateFileWithCursor = `<div (click)="ti¦tle = 'newtitle'"></div>`;
const {text, cursor} = extractCursorInfo(templateFileWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile], [templateFile]);
env = createModuleWithDeclarations([appFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
@ -142,7 +142,7 @@ describe('find references', () => {
name: _('/app.ts'),
contents: text,
};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
@ -162,7 +162,7 @@ describe('find references', () => {
name: _('/app.ts'),
contents: text,
};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
// 3 references: the type definition, the value assignment, and the read in the template
expect(refs.length).toBe(3);
@ -191,7 +191,7 @@ describe('find references', () => {
const templateFileWithCursor = `<div (click)="hero['name'] = bat¦man"></div>`;
const {text, cursor} = extractCursorInfo(templateFileWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile], [templateFile]);
env = createModuleWithDeclarations([appFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
@ -209,7 +209,7 @@ describe('find references', () => {
title = '';
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertTextSpans(refs, ['myInput']);
@ -236,7 +236,7 @@ describe('find references', () => {
};
const {text, cursor} = extractCursorInfo(templateWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile], [templateFile]);
env = createModuleWithDeclarations([appFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertTextSpans(refs, ['myTemplate']);
@ -274,7 +274,7 @@ describe('find references', () => {
const templateWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirR¦ef }}';
const {text, cursor} = extractCursorInfo(templateWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile, dirFile], [templateFile]);
env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.html']);
@ -285,7 +285,7 @@ describe('find references', () => {
const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef.dirV¦alue }}';
const {text, cursor} = extractCursorInfo(fileWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile, dirFile], [templateFile]);
env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['dir.ts', 'app.html']);
@ -296,7 +296,7 @@ describe('find references', () => {
const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef?.dirV¦alue }}';
const {text, cursor} = extractCursorInfo(fileWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile, dirFile], [templateFile]);
env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['dir.ts', 'app.html']);
@ -307,7 +307,7 @@ describe('find references', () => {
const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef?.doSometh¦ing() }}';
const {text, cursor} = extractCursorInfo(fileWithCursor);
const templateFile = {name: _('/app.html'), contents: text};
createModuleWithDeclarations([appFile, dirFile], [templateFile]);
env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['dir.ts', 'app.html']);
@ -326,7 +326,7 @@ describe('find references', () => {
heroes: string[] = [];
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.ts']);
@ -347,7 +347,7 @@ describe('find references', () => {
heroes: string[] = [];
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.ts']);
@ -408,7 +408,7 @@ describe('find references', () => {
heroes: Array<{name: string}> = [];
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile]);
env = createModuleWithDeclarations([appFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.ts']);
@ -444,7 +444,7 @@ describe('find references', () => {
`;
const {text, cursor} = extractCursorInfo(appContentsWithCursor);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile, prefixPipeFile]);
env = createModuleWithDeclarations([appFile, prefixPipeFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(5);
assertFileNames(refs, ['index.d.ts', 'prefix-pipe.ts', 'app.ts']);
@ -463,7 +463,7 @@ describe('find references', () => {
`;
const {text, cursor} = extractCursorInfo(appContentsWithCursor);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile, prefixPipeFile]);
env = createModuleWithDeclarations([appFile, prefixPipeFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(2);
assertFileNames(refs, ['app.ts']);
@ -490,7 +490,7 @@ describe('find references', () => {
title = 'title';
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile, stringModelTestFile]);
env = createModuleWithDeclarations([appFile, stringModelTestFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toEqual(2);
assertFileNames(refs, ['string-model.ts', 'app.ts']);
@ -507,7 +507,7 @@ describe('find references', () => {
title = 'title';
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile, stringModelTestFile]);
env = createModuleWithDeclarations([appFile, stringModelTestFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toEqual(2);
assertFileNames(refs, ['string-model.ts', 'app.ts']);
@ -534,7 +534,7 @@ describe('find references', () => {
title = 'title';
}`,
};
createModuleWithDeclarations([appFile, stringModelTestFile]);
env = createModuleWithDeclarations([appFile, stringModelTestFile]);
const refs = getReferencesAtPosition(_('/string-model.ts'), cursor)!;
expect(refs.length).toEqual(2);
assertFileNames(refs, ['app.ts', 'string-model.ts']);
@ -576,7 +576,7 @@ describe('find references', () => {
title = 'title';
}`,
};
createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]);
env = createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]);
const refs = getReferencesAtPosition(_('/other-dir.ts'), cursor)!;
expect(refs.length).toEqual(3);
assertFileNames(refs, ['app.ts', 'string-model.ts', 'other-dir.ts']);
@ -593,7 +593,7 @@ describe('find references', () => {
title = 'title';
}`);
const appFile = {name: _('/app.ts'), contents: text};
createModuleWithDeclarations([appFile, stringModelTestFile]);
env = createModuleWithDeclarations([appFile, stringModelTestFile]);
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toEqual(2);
assertFileNames(refs, ['string-model.ts', 'app.ts']);
@ -703,39 +703,6 @@ describe('find references', () => {
entry.originalContextSpan ? getText(fileContents, entry.originalContextSpan) : undefined,
};
}
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();
}
function createModuleWithDeclarations(
filesWithClassDeclarations: TestFile[], externalResourceFiles: TestFile[] = []): void {
const externalClasses =
filesWithClassDeclarations.map(file => getFirstClassDeclaration(file.contents));
const externalImports = filesWithClassDeclarations.map(file => {
const className = getFirstClassDeclaration(file.contents);
const fileName = last(file.name.split('/')).replace('.ts', '');
return `import {${className}} from './${fileName}';`;
});
const contents = `
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
${externalImports.join('\n')}
@NgModule({
declarations: [${externalClasses.join(',')}],
imports: [CommonModule],
})
export class AppModule {}
`;
const moduleFile = {name: _('/app-module.ts'), contents, isRoot: true};
env = LanguageServiceTestEnvironment.setup(
[moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]);
}
});
function assertFileNames(refs: Array<{fileName: string}>, expectedFileNames: string[]) {

View File

@ -6,7 +6,48 @@
* found in the LICENSE file at https://angular.io/license
*/
import {absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system';
import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env';
export function getText(contents: string, textSpan: ts.TextSpan) {
return contents.substr(textSpan.start, textSpan.length);
}
function last<T>(array: T[]): T {
return array[array.length - 1];
}
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 createModuleWithDeclarations(
filesWithClassDeclarations: TestFile[],
externalResourceFiles: TestFile[] = []): LanguageServiceTestEnvironment {
const externalClasses =
filesWithClassDeclarations.map(file => getFirstClassDeclaration(file.contents));
const externalImports = filesWithClassDeclarations.map(file => {
const className = getFirstClassDeclaration(file.contents);
const fileName = last(file.name.split('/')).replace('.ts', '');
return `import {${className}} from './${fileName}';`;
});
const contents = `
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
${externalImports.join('\n')}
@NgModule({
declarations: [${externalClasses.join(',')}],
imports: [CommonModule],
})
export class AppModule {}
`;
const moduleFile = {name: _('/app-module.ts'), contents, isRoot: true};
return LanguageServiceTestEnvironment.setup(
[moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]);
}