test(language-service): [ivy] Add mock service to overwrite files (#36923)
Add a mechanism to replace file contents for a specific file. This allows us to write custom test scenarios in code without modifying the test project. Since we are no longer mocking the language service host, the file overwrite needs to happen via the project service. Project service manages a set of script infos, and overwriting the files is a matter of updating the relevant script infos. Note that the actual project service is wrapped inside a Mock Service. Tests should not have direct access to the project service. All manipulations should take place via the Mock Service. The MockService provides a `reset()` method to undo temporary overwrites after each test. PR Close #36923
This commit is contained in:
parent
1b8752e595
commit
da79e0433f
|
@ -36,6 +36,10 @@ export const APP_COMPONENT = join(PROJECT_DIR, 'app', 'app.component.ts');
|
|||
export const APP_MAIN = join(PROJECT_DIR, 'app', 'main.ts');
|
||||
export const PARSING_CASES = join(PROJECT_DIR, 'app', 'parsing-cases.ts');
|
||||
|
||||
const NOOP_FILE_WATCHER: ts.FileWatcher = {
|
||||
close() {}
|
||||
};
|
||||
|
||||
export const host: ts.server.ServerHost = {
|
||||
...ts.sys,
|
||||
readFile(absPath: string, encoding?: string): string |
|
||||
|
@ -44,10 +48,25 @@ export const host: ts.server.ServerHost = {
|
|||
// MockTypescriptHost
|
||||
return ts.sys.readFile(absPath, encoding);
|
||||
},
|
||||
// TODO: Need to cast as never because this is not a proper ServerHost interface.
|
||||
// ts.sys lacks methods like watchFile() and watchDirectory(), but these are not
|
||||
// needed for now.
|
||||
} as never;
|
||||
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher {
|
||||
return NOOP_FILE_WATCHER;
|
||||
},
|
||||
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback): ts.FileWatcher {
|
||||
return NOOP_FILE_WATCHER;
|
||||
},
|
||||
setTimeout() {
|
||||
throw new Error('setTimeout is not implemented');
|
||||
},
|
||||
clearTimeout() {
|
||||
throw new Error('clearTimeout is not implemented');
|
||||
},
|
||||
setImmediate() {
|
||||
throw new Error('setImmediate is not implemented');
|
||||
},
|
||||
clearImmediate() {
|
||||
throw new Error('clearImmediate is not implemented');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a ConfiguredProject and an actual program for the test project located
|
||||
|
@ -73,5 +92,61 @@ export function setup() {
|
|||
}
|
||||
// The following operation forces a ts.Program to be created.
|
||||
const tsLS = project.getLanguageService();
|
||||
return {projectService, project, tsLS};
|
||||
return {
|
||||
service: new MockService(project, projectService),
|
||||
project,
|
||||
tsLS,
|
||||
};
|
||||
}
|
||||
|
||||
class MockService {
|
||||
private readonly overwritten = new Set<ts.server.NormalizedPath>();
|
||||
|
||||
constructor(
|
||||
private readonly project: ts.server.Project,
|
||||
private readonly ps: ts.server.ProjectService,
|
||||
) {}
|
||||
|
||||
overwrite(fileName: string, newText: string): string {
|
||||
const scriptInfo = this.getScriptInfo(fileName);
|
||||
this.overwritten.add(scriptInfo.fileName);
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
scriptInfo.editContent(0, snapshot.getLength(), preprocess(newText));
|
||||
const sameProgram = this.project.updateGraph(); // clear the dirty flag
|
||||
if (sameProgram) {
|
||||
throw new Error('Project should have updated program after overwrite');
|
||||
}
|
||||
return newText;
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.overwritten.size === 0) {
|
||||
return;
|
||||
}
|
||||
for (const fileName of this.overwritten) {
|
||||
const scriptInfo = this.getScriptInfo(fileName);
|
||||
const reloaded = scriptInfo.reloadFromFile();
|
||||
if (!reloaded) {
|
||||
throw new Error(`Failed to reload ${scriptInfo.fileName}`);
|
||||
}
|
||||
}
|
||||
const sameProgram = this.project.updateGraph();
|
||||
if (sameProgram) {
|
||||
throw new Error('Project should have updated program after reset');
|
||||
}
|
||||
this.overwritten.clear();
|
||||
}
|
||||
|
||||
getScriptInfo(fileName: string): ts.server.ScriptInfo {
|
||||
const scriptInfo = this.ps.getScriptInfo(fileName);
|
||||
if (!scriptInfo) {
|
||||
throw new Error(`No existing script info for ${fileName}`);
|
||||
}
|
||||
return scriptInfo;
|
||||
}
|
||||
}
|
||||
|
||||
const REGEX_CURSOR = /¦/g;
|
||||
function preprocess(text: string): string {
|
||||
return text.replace(REGEX_CURSOR, '');
|
||||
}
|
||||
|
|
|
@ -11,9 +11,17 @@ import * as ts from 'typescript/lib/tsserverlibrary';
|
|||
import {APP_MAIN, setup, TEST_SRCDIR} from './mock_host';
|
||||
|
||||
describe('mock host', () => {
|
||||
const {project, service, tsLS} = setup();
|
||||
|
||||
beforeEach(() => {
|
||||
service.reset();
|
||||
});
|
||||
|
||||
it('can load test project from Bazel runfiles', () => {
|
||||
const {project, tsLS} = setup();
|
||||
expect(project).toBeInstanceOf(ts.server.ConfiguredProject);
|
||||
const configPath = (project as ts.server.ConfiguredProject).getConfigFilePath();
|
||||
expect(configPath.substring(TEST_SRCDIR.length))
|
||||
.toBe('/angular/packages/language-service/test/project/tsconfig.json');
|
||||
const program = tsLS.getProgram();
|
||||
expect(program).toBeDefined();
|
||||
const sourceFiles = program!.getSourceFiles().map(sf => {
|
||||
|
@ -36,7 +44,6 @@ describe('mock host', () => {
|
|||
});
|
||||
|
||||
it('produces no TS error for test project', () => {
|
||||
const {project, tsLS} = setup();
|
||||
const errors = project.getAllProjectErrors();
|
||||
expect(errors).toEqual([]);
|
||||
const globalErrors = project.getGlobalProjectErrors();
|
||||
|
@ -44,4 +51,24 @@ describe('mock host', () => {
|
|||
const diags = tsLS.getSemanticDiagnostics(APP_MAIN);
|
||||
expect(diags).toEqual([]);
|
||||
});
|
||||
|
||||
it('can overwrite test file', () => {
|
||||
service.overwrite(APP_MAIN, `const x: string = 0`);
|
||||
const scriptInfo = service.getScriptInfo(APP_MAIN);
|
||||
expect(getText(scriptInfo)).toBe('const x: string = 0');
|
||||
});
|
||||
|
||||
it('can find the cursor', () => {
|
||||
const content = service.overwrite(APP_MAIN, `const fo¦o = 'hello world';`);
|
||||
// content returned by overwrite() is the original content with cursor
|
||||
expect(content).toBe(`const fo¦o = 'hello world';`);
|
||||
const scriptInfo = service.getScriptInfo(APP_MAIN);
|
||||
// script info content should not contain cursor
|
||||
expect(getText(scriptInfo)).toBe(`const foo = 'hello world';`);
|
||||
});
|
||||
});
|
||||
|
||||
function getText(scriptInfo: ts.server.ScriptInfo): string {
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
return snapshot.getText(0, snapshot.getLength());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue