Skip to content

Commit

Permalink
add API tests for typescript language
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Mar 4, 2020
1 parent 313d31c commit a0e57d0
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 40 deletions.
22 changes: 11 additions & 11 deletions examples/api-tests/src/saveable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ describe('Saveable', function () {
assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`);
await Saveable.save(widget);
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`);
assert.equal(editor.getControl().getValue(), edit, `model should be updated with '${edit}'`);
assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`);
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, edit, `fs should be updated with '${edit}'`);
assert.equal(state.content.trimRight(), edit, `fs should be updated with '${edit}'`);
}
});

Expand Down Expand Up @@ -129,7 +129,7 @@ describe('Saveable', function () {
assert.isTrue(outOfSync, 'file should be out of sync');
assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
assert.equal(editor.getControl().getValue(), longContent.substring(3), 'model should be updated');
assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated');
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'baz', 'fs should NOT be updated');
});
Expand All @@ -151,7 +151,7 @@ describe('Saveable', function () {
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
let state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'baz', 'fs should NOT be updated');

Expand All @@ -164,9 +164,9 @@ describe('Saveable', function () {
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'bar', 'fs should be updated');
assert.equal(state.content.trimRight(), 'bar', 'fs should be updated');
});

it('accept new save', async () => {
Expand All @@ -181,9 +181,9 @@ describe('Saveable', function () {
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.equal(editor.getControl().getValue(), 'bar', 'model should be updated');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'bar', 'fs should be updated');
assert.equal(state.content.trimRight(), 'bar', 'fs should be updated');
});

it('cancel save on close', async () => {
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('Saveable', function () {
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'bar', 'fs should be updated');
assert.equal(state.content.trimRight(), 'bar', 'fs should be updated');
});

it('normal close', async () => {
Expand All @@ -254,7 +254,7 @@ describe('Saveable', function () {
});
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'bar', 'fs should be updated');
assert.equal(state.content.trimRight(), 'bar', 'fs should be updated');
});

it('delete file for saved', async () => {
Expand Down Expand Up @@ -320,7 +320,7 @@ describe('Saveable', function () {
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.isTrue(editor.document.valid, 'should be valid after save');
const state = await fileSystem.resolveContent(fileUri.toString());
assert.equal(state.content, 'bar', 'fs should be updated');
assert.equal(state.content.trimRight(), 'bar', 'fs should be updated');
});

it('move file for saved', async function () {
Expand Down
92 changes: 92 additions & 0 deletions examples/api-tests/src/typescript.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/********************************************************************************
* Copyright (C) 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// @ts-check
describe('TypeScript', function () {
const { assert } = chai;

const Uri = require('@theia/core/lib/common/uri');
const { BrowserMainMenuFactory } = require('@theia/core/lib/browser/menu/browser-menu-plugin');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { EDITOR_CONTEXT_MENU } = require('@theia/editor/lib/browser/editor-menu');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');

/** @type {import('inversify').Container} */
const container = window['theia'].container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const menuFactory = container.get(BrowserMainMenuFactory);
const pluginService = container.get(HostedPluginSupport);
const contextKeyService = container.get(ContextKeyService);

const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri);
const fileUri = rootUri.resolve('src-gen/backend/server.js');

/** @type {EditorWidget} */
let widget;
/** @type {MonacoEditor} */
let editor;

before(async function () {
this.timeout(5000);
await Promise.all([
pluginService.load(),
editorManager.closeAll({ save: false })
]);
await Promise.all([
(async () => {
const plugin = pluginService.plugins.find(p => p.model.id === 'vscode.typescript-language-features');
await pluginService.activatePlugin(plugin.model.id);
})(),
(async () => {
widget = await editorManager.open(fileUri, { mode: 'activate' });
editor = MonacoEditor.get(widget);
})()
]);
// wait till tsserver is running, see:
// https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103
await new Promise(resolve => {
if (contextKeyService.match('typescript.isManagedFile')) {
resolve();
return;
}
contextKeyService.onDidChange(e => {
if (contextKeyService.match('typescript.isManagedFile')) {
resolve();
}
});
});
});

after(async () => {
widget = undefined;
editor = undefined;
await editorManager.closeAll({ save: false });
});

it('document formating should be visible and enabled in the editor context menu', () => {
const menu = menuFactory.createContextMenu(EDITOR_CONTEXT_MENU);
const item = menu.items.find(i => i.command === 'editor.action.formatDocument');
assert.isDefined(item);
assert.isTrue(item.isVisible);
assert.isTrue(item.isEnabled);
});

});
2 changes: 1 addition & 1 deletion examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"watch": "yarn build --watch",
"start": "theia start --plugins=local-dir:../../plugins",
"start:debug": "yarn start --log-level=debug",
"test": "theia test . --test-spec=../api-tests/**/*.spec.js",
"test": "theia test . --plugins=local-dir:../../plugins --test-spec=../api-tests/**/*.spec.js",
"test:debug": "yarn test --test-inspect",
"coverage": "yarn test --test-coverage && yarn coverage:report",
"coverage:report": "nyc report --reporter=html",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export interface PluginManagerExt {
$updateStoragePath(path: string | undefined): Promise<void>;

$activateByEvent(event: string): Promise<void>;

$activatePlugin(id: string): Promise<void>;
}

export interface CommandRegistryMain {
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,14 @@ export class HostedPluginSupport {
}
}

async activatePlugin(id: string): Promise<void> {
const activation = [];
for (const manager of this.managers.values()) {
activation.push(manager.$activatePlugin(id));
}
await Promise.all(activation);
}

protected createMeasurement(name: string): () => number {
const startMarker = `${name}-start`;
const endMarker = `${name}-end`;
Expand Down
72 changes: 44 additions & 28 deletions packages/plugin-ext/src/plugin/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
'onWebviewPanel'
]);

private configStorage: ConfigStorage | undefined;
private readonly registry = new Map<string, Plugin>();
private readonly activations = new Map<string, (() => Promise<void>)[] | undefined>();
/** promises to whether loading each plugin has been successful */
Expand Down Expand Up @@ -196,14 +197,16 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
}

async $start(params: PluginManagerStartParams): Promise<void> {
this.configStorage = params.configStorage;

const [plugins, foreignPlugins] = await this.host.init(params.plugins);
// add foreign plugins
for (const plugin of foreignPlugins) {
this.registerPlugin(plugin, params.configStorage);
this.registerPlugin(plugin);
}
// add own plugins, before initialization
for (const plugin of plugins) {
this.registerPlugin(plugin, params.configStorage);
this.registerPlugin(plugin);
}

// run eager plugins
Expand All @@ -219,15 +222,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
this.fireOnDidChange();
}

protected registerPlugin(plugin: Plugin, configStorage: ConfigStorage): void {
protected registerPlugin(plugin: Plugin): void {
this.registry.set(plugin.model.id, plugin);
if (plugin.pluginPath && Array.isArray(plugin.rawModel.activationEvents)) {
const activation = async () => {
const title = `Activating ${plugin.model.displayName || plugin.model.name}`;
const id = await this.notificationMain.$startProgress({ title, location: 'window' });
await this.loadPlugin(plugin, configStorage);
this.notificationMain.$stopProgress(id);
};
const activation = () => this.$activatePlugin(plugin.model.id);
// an internal activation event is a subject to change
this.setActivation(`onPlugin:${plugin.model.id}`, activation);
const unsupportedActivationEvents = plugin.rawModel.activationEvents.filter(e => !PluginManagerExtImpl.SUPPORTED_ACTIVATION_EVENTS.has(e.split(':')[0]));
Expand Down Expand Up @@ -261,38 +259,49 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
let loading = this.loadedPlugins.get(plugin.model.id);
if (!loading) {
loading = (async () => {
if (plugin.rawModel.extensionDependencies) {
for (const dependencyId of plugin.rawModel.extensionDependencies) {
const dependency = this.registry.get(dependencyId.toLowerCase());
const id = plugin.model.displayName || plugin.model.id;
if (dependency) {
const depId = dependency.model.displayName || dependency.model.id;
const loadedSuccessfully = await this.loadPlugin(dependency, configStorage, visited);
if (!loadedSuccessfully) {
const message = `Cannot activate extension '${id}' because it depends on extension '${depId}', which failed to activate.`;
const progressId = await this.notificationMain.$startProgress({
title: `Activating ${plugin.model.displayName || plugin.model.name}`,
location: 'window'
});
try {
if (plugin.rawModel.extensionDependencies) {
for (const dependencyId of plugin.rawModel.extensionDependencies) {
const dependency = this.registry.get(dependencyId.toLowerCase());
const id = plugin.model.displayName || plugin.model.id;
if (dependency) {
const depId = dependency.model.displayName || dependency.model.id;
const loadedSuccessfully = await this.loadPlugin(dependency, configStorage, visited);
if (!loadedSuccessfully) {
const message = `Cannot activate extension '${id}' because it depends on extension '${depId}', which failed to activate.`;
this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []);
return false;
}
} else {
const message = `Cannot activate the '${id}' extension because it depends on the '${dependencyId}' extension, which is not installed.`;
this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []);
console.warn(message);
return false;
}
} else {
const message = `Cannot activate the '${id}' extension because it depends on the '${dependencyId}' extension, which is not installed.`;
this.messageRegistryProxy.$showMessage(MainMessageType.Error, message, {}, []);
console.warn(message);
return false;
}
}
}

let pluginMain = this.host.loadPlugin(plugin);
// see https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/workbench/api/common/extHostExtensionService.ts#L372-L376
pluginMain = pluginMain || {};
return this.startPlugin(plugin, configStorage, pluginMain);
let pluginMain = this.host.loadPlugin(plugin);
// see https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/workbench/api/common/extHostExtensionService.ts#L372-L376
pluginMain = pluginMain || {};
return await this.startPlugin(plugin, configStorage, pluginMain);
} finally {
this.notificationMain.$stopProgress(progressId);
}
})();
}
this.loadedPlugins.set(plugin.model.id, loading);
return loading;
}

async $updateStoragePath(path: string | undefined): Promise<void> {
if (this.configStorage) {
this.configStorage.hostStoragePath = path;
}
this.pluginContextsMap.forEach((pluginContext: theia.PluginContext, pluginId: string) => {
pluginContext.storagePath = path ? join(path, pluginId) : undefined;
});
Expand All @@ -309,6 +318,13 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
}
}

async $activatePlugin(id: string): Promise<void> {
const plugin = this.registry.get(id);
if (plugin && this.configStorage) {
await this.loadPlugin(plugin, this.configStorage);
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async startPlugin(plugin: Plugin, configStorage: ConfigStorage, pluginMain: any): Promise<boolean> {
const subscriptions: theia.Disposable[] = [];
Expand Down

0 comments on commit a0e57d0

Please sign in to comment.