@theia/playwright 1.23.0-next.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ <div align='center'>
2
+
3
+ <br />
4
+
5
+ <img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
6
+
7
+ <h2>ECLIPSE THEIA - PLAYWRIGHT</h2>
8
+
9
+ <hr />
10
+
11
+ </div>
12
+
13
+ ## Description
14
+
15
+ Theia 🎭 Playwright is a [page object](https://martinfowler.com/bliki/PageObject.html) framework based on [Playwright](https://github.com/microsoft/playwright) for developing system tests of [Theia](https://github.com/eclipse-theia/theia)-based applications. See it in action below.
16
+
17
+ <div style='margin:0 auto;width:70%;'>
18
+
19
+ ![Theia System Testing in Action](./docs/images/teaser.gif)
20
+
21
+ </div>
22
+
23
+ The Theia 🎭 Playwright page objects introduce abstraction over Theia's user interfaces, encapsulating the details of the user interface interactions, wait conditions, etc., to help keeping your tests more concise, maintainable, and stable.
24
+ Ready for an [example](./docs/GETTING_STARTED.md)?
25
+
26
+ The actual interaction with the Theia application is implemented with 🎭 Playwright in Typescript. Thus, we can take advantage of [Playwright's benefits](https://playwright.dev/docs/why-playwright/) and run or debug tests headless or headful across all modern browsers.
27
+ Check out [Playwright's documentation](https://playwright.dev/docs/intro) for more information.
28
+
29
+ This page object framework not only covers Theia's generic capabilities, such as handling views, the quick command palette, file explorer etc.
30
+ It is [extensible](./docs/EXTENSIBILITY.md) so you can add dedicated page objects for custom Theia components, such as custom views, editors, menus, etc.
31
+
32
+ ## Documentation
33
+
34
+ - [Getting Started](./docs/GETTING_STARTED.md)
35
+ - [Extensibility](./docs/EXTENSIBILITY.md)
36
+ - [Building and Developing Theia 🎭 Playwright](./docs/DEVELOPING.md)
37
+
38
+ ## Additional Information
39
+
40
+ - [Theia - GitHub](https://github.com/eclipse-theia/theia)
41
+ - [Theia - Website](https://theia-ide.org/)
42
+ - [Playwright - GitHub](https://github.com/microsoft/playwright)
43
+ - [Playwright - Website](https://playwright.dev)
44
+
45
+ ## License
46
+
47
+ - [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
48
+ - [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
49
+
50
+ ## Trademark
51
+
52
+ "Theia" is a trademark of the Eclipse Foundation
53
+ https://www.eclipse.org/theia
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@theia/playwright",
3
+ "version": "1.23.0-next.26+0aa2621d3fd",
4
+ "description": "System tests for Theia",
5
+ "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/eclipse-theia/theia.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/eclipse-theia/theia/issues"
12
+ },
13
+ "homepage": "https://github.com/eclipse-theia/theia",
14
+ "scripts": {
15
+ "prepare": "yarn clean && yarn build",
16
+ "clean": "rimraf lib",
17
+ "build": "tsc --incremental && yarn lint && npx playwright install chromium",
18
+ "theia:start": "yarn --cwd ../browser start",
19
+ "lint": "eslint -c ./.eslintrc.js --ext .ts ./src ./tests",
20
+ "lint:fix": "eslint -c ./.eslintrc.js --ext .ts ./src ./tests --fix",
21
+ "ui-tests": "yarn && playwright test --config=./configs/playwright.config.ts",
22
+ "ui-tests-ci": "yarn && playwright test --config=./configs/playwright.ci.config.ts",
23
+ "ui-tests-headful": "yarn && playwright test --config=./configs/playwright.headful.config.ts",
24
+ "ui-tests-report-generate": "allure generate ./allure-results --clean -o allure-results/allure-report",
25
+ "ui-tests-report": "yarn ui-tests-report-generate && allure open allure-results/allure-report"
26
+ },
27
+ "files": [
28
+ "src"
29
+ ],
30
+ "dependencies": {
31
+ "@playwright/test": "1.17.1",
32
+ "fs-extra": "^9.0.8"
33
+ },
34
+ "devDependencies": {
35
+ "@types/fs-extra": "^9.0.8",
36
+ "allure-commandline": "^2.13.8",
37
+ "allure-playwright": "^2.0.0-beta.14"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "gitHead": "0aa2621d3fd7ce5f1dff771506f5473200104fea"
43
+ }
@@ -0,0 +1,26 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { TheiaDialog } from './theia-dialog';
18
+
19
+ export class TheiaAboutDialog extends TheiaDialog {
20
+
21
+ async isVisible(): Promise<boolean> {
22
+ const dialog = await this.page.$(`${this.blockSelector} .theia-aboutDialog`);
23
+ return !!dialog && dialog.isVisible();
24
+ }
25
+
26
+ }
@@ -0,0 +1,136 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { Page } from '@playwright/test';
18
+ import { TheiaEditor } from './theia-editor';
19
+ import { DOT_FILES_FILTER, TheiaExplorerView } from './theia-explorer-view';
20
+ import { TheiaMenuBar } from './theia-main-menu';
21
+ import { TheiaPreferenceScope, TheiaPreferenceView } from './theia-preference-view';
22
+ import { TheiaQuickCommandPalette } from './theia-quick-command-palette';
23
+ import { TheiaStatusBar } from './theia-status-bar';
24
+ import { TheiaView } from './theia-view';
25
+ import { TheiaWorkspace } from './theia-workspace';
26
+
27
+ export class TheiaApp {
28
+
29
+ readonly statusBar = new TheiaStatusBar(this);
30
+ readonly quickCommandPalette = new TheiaQuickCommandPalette(this);
31
+ readonly menuBar = new TheiaMenuBar(this);
32
+ public workspace: TheiaWorkspace;
33
+
34
+ public static async load(page: Page, initialWorkspace?: TheiaWorkspace): Promise<TheiaApp> {
35
+ const app = new TheiaApp(page, initialWorkspace);
36
+ await TheiaApp.loadOrReload(page, '/#' + app.workspace.urlEncodedPath);
37
+ await page.waitForSelector('.theia-preload', { state: 'detached' });
38
+ await page.waitForSelector('.theia-ApplicationShell');
39
+ await app.waitForInitialized();
40
+ return Promise.resolve(app);
41
+ }
42
+
43
+ protected static async loadOrReload(page: Page, url: string): Promise<void> {
44
+ if (page.url() === url) {
45
+ await page.reload();
46
+ } else {
47
+ const wasLoadedAlready = await page.isVisible('.theia-ApplicationShell');
48
+ await page.goto(url);
49
+ if (wasLoadedAlready) {
50
+ // Theia doesn't refresh on URL change only
51
+ // So we need to reload if the app was already loaded before
52
+ await page.reload();
53
+ }
54
+ }
55
+ }
56
+
57
+ protected constructor(public page: Page, initialWorkspace?: TheiaWorkspace) {
58
+ this.workspace = initialWorkspace ? initialWorkspace : new TheiaWorkspace();
59
+ this.workspace.initialize();
60
+ }
61
+
62
+ async isMainContentPanelVisible(): Promise<boolean> {
63
+ const contentPanel = await this.page.$('#theia-main-content-panel');
64
+ return !!contentPanel && contentPanel.isVisible();
65
+ }
66
+
67
+ async openPreferences(viewFactory: { new(app: TheiaApp): TheiaPreferenceView }, preferenceScope = TheiaPreferenceScope.Workspace): Promise<TheiaPreferenceView> {
68
+ const view = new viewFactory(this);
69
+ if (await view.isTabVisible()) {
70
+ await view.activate();
71
+ return view;
72
+ }
73
+ await view.open(preferenceScope);
74
+ return view;
75
+ }
76
+
77
+ async openView<T extends TheiaView>(viewFactory: { new(app: TheiaApp): T }): Promise<T> {
78
+ const view = new viewFactory(this);
79
+ if (await view.isTabVisible()) {
80
+ await view.activate();
81
+ return view;
82
+ }
83
+ await view.open();
84
+ return view;
85
+ }
86
+
87
+ async openEditor<T extends TheiaEditor>(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T },
88
+ editorName?: string, expectFileNodes = true): Promise<T> {
89
+ const explorer = await this.openView(TheiaExplorerView);
90
+ if (!explorer) {
91
+ throw Error('TheiaExplorerView could not be opened.');
92
+ }
93
+ if (expectFileNodes) {
94
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
95
+ if (fileStatElements.length < 1) {
96
+ throw Error('TheiaExplorerView is empty.');
97
+ }
98
+ }
99
+ const fileNode = await explorer.fileStatNode(filePath);
100
+ if (!fileNode || ! await fileNode?.isFile()) {
101
+ throw Error(`Specified path '${filePath}' could not be found or isn't a file.`);
102
+ }
103
+
104
+ const editor = new editorFactory(filePath, this);
105
+ const contextMenu = await fileNode.openContextMenu();
106
+ const editorToUse = editorName ? editorName : editor.name ? editor.name : undefined;
107
+ if (editorToUse) {
108
+ const menuItem = await contextMenu.menuItemByNamePath('Open With', editorToUse);
109
+ if (!menuItem) {
110
+ throw Error(`Editor named '${editorName}' could not be found in "Open With" menu.`);
111
+ }
112
+ await menuItem.click();
113
+ } else {
114
+ await contextMenu.clickMenuItem('Open');
115
+ }
116
+
117
+ await editor.waitForVisible();
118
+ return editor;
119
+ }
120
+
121
+ async activateExistingEditor<T extends TheiaEditor>(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T }): Promise<T> {
122
+ const editor = new editorFactory(filePath, this);
123
+ if (!await editor.isTabVisible()) {
124
+ throw new Error(`Could not find opened editor for file ${filePath}`);
125
+ }
126
+ await editor.activate();
127
+ await editor.waitForVisible();
128
+ return editor;
129
+ }
130
+
131
+ /** Specific Theia apps may add additional conditions to wait for. */
132
+ async waitForInitialized(): Promise<void> {
133
+ // empty by default
134
+ }
135
+
136
+ }
@@ -0,0 +1,42 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+
19
+ import { TheiaApp } from './theia-app';
20
+ import { TheiaMenu } from './theia-menu';
21
+
22
+ export class TheiaContextMenu extends TheiaMenu {
23
+
24
+ public static async openAt(app: TheiaApp, x: number, y: number): Promise<TheiaContextMenu> {
25
+ await app.page.mouse.move(x, y);
26
+ await app.page.mouse.click(x, y, { button: 'right' });
27
+ return TheiaContextMenu.returnWhenVisible(app);
28
+ }
29
+
30
+ public static async open(app: TheiaApp, element: () => Promise<ElementHandle<SVGElement | HTMLElement>>): Promise<TheiaContextMenu> {
31
+ const elementHandle = await element();
32
+ await elementHandle.click({ button: 'right' });
33
+ return TheiaContextMenu.returnWhenVisible(app);
34
+ }
35
+
36
+ private static async returnWhenVisible(app: TheiaApp): Promise<TheiaContextMenu> {
37
+ const menu = new TheiaContextMenu(app);
38
+ await menu.waitForVisible();
39
+ return menu;
40
+ }
41
+
42
+ }
@@ -0,0 +1,113 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+ import { TheiaPageObject } from './theia-page-object';
19
+
20
+ export class TheiaDialog extends TheiaPageObject {
21
+
22
+ protected overlaySelector = '#theia-dialog-shell';
23
+ protected blockSelector = this.overlaySelector + ' .dialogBlock';
24
+ protected titleBarSelector = this.blockSelector + ' .dialogTitle';
25
+ protected titleSelector = this.titleBarSelector + ' > div';
26
+ protected contentSelector = this.blockSelector + ' .dialogContent > div';
27
+ protected controlSelector = this.blockSelector + ' .dialogControl';
28
+
29
+ async waitForVisible(): Promise<void> {
30
+ await this.page.waitForSelector(`${this.blockSelector}`, { state: 'visible' });
31
+ }
32
+
33
+ async waitForClosed(): Promise<void> {
34
+ await this.page.waitForSelector(`${this.blockSelector}`, { state: 'detached' });
35
+ }
36
+
37
+ async isVisible(): Promise<boolean> {
38
+ const pouDialogElement = await this.page.$(this.blockSelector);
39
+ return pouDialogElement ? pouDialogElement.isVisible() : false;
40
+ }
41
+
42
+ async title(): Promise<string | null> {
43
+ const titleElement = await this.page.waitForSelector(`${this.titleSelector}`);
44
+ return titleElement.textContent();
45
+ }
46
+
47
+ async waitUntilTitleIsDisplayed(title: string): Promise<void> {
48
+ await this.page.waitForFunction(predicate => {
49
+ const element = document.querySelector(predicate.titleSelector);
50
+ return !!element && element.textContent === predicate.expectedTitle;
51
+ }, { titleSelector: this.titleSelector, expectedTitle: title });
52
+ }
53
+
54
+ protected async contentElement(): Promise<ElementHandle<SVGElement | HTMLElement>> {
55
+ return this.page.waitForSelector(this.contentSelector);
56
+ }
57
+
58
+ protected async buttonElement(label: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
59
+ return this.page.waitForSelector(`${this.controlSelector} button:has-text("${label}")`);
60
+ }
61
+
62
+ protected async buttonElementByClass(buttonClass: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
63
+ return this.page.waitForSelector(`${this.controlSelector} button${buttonClass}`);
64
+ }
65
+
66
+ protected async validationElement(): Promise<ElementHandle<SVGElement | HTMLElement>> {
67
+ return this.page.waitForSelector(`${this.controlSelector} div.error`);
68
+ }
69
+
70
+ async getValidationText(): Promise<string | null> {
71
+ const element = await this.validationElement();
72
+ return element.textContent();
73
+ }
74
+
75
+ async validationResult(): Promise<boolean> {
76
+ const validationText = await this.getValidationText();
77
+ return validationText !== '' ? false : true;
78
+ }
79
+
80
+ async close(): Promise<void> {
81
+ const closeButton = await this.page.waitForSelector(`${this.titleBarSelector} i.closeButton`);
82
+ await closeButton.click();
83
+ await this.waitForClosed();
84
+ }
85
+
86
+ async clickButton(buttonLabel: string): Promise<void> {
87
+ const buttonElement = await this.buttonElement(buttonLabel);
88
+ await buttonElement.click();
89
+ }
90
+
91
+ async isButtonDisabled(buttonLabel: string): Promise<boolean> {
92
+ const buttonElement = await this.buttonElement(buttonLabel);
93
+ return buttonElement.isDisabled();
94
+ }
95
+
96
+ async clickMainButton(): Promise<void> {
97
+ const buttonElement = await this.buttonElementByClass('.theia-button.main');
98
+ await buttonElement.click();
99
+ }
100
+
101
+ async clickSecondaryButton(): Promise<void> {
102
+ const buttonElement = await this.buttonElementByClass('.theia-button.secondary');
103
+ await buttonElement.click();
104
+ }
105
+
106
+ async waitUntilMainButtonIsEnabled(): Promise<void> {
107
+ await this.page.waitForFunction(() => {
108
+ const button = document.querySelector<HTMLButtonElement>(`${this.controlSelector} > button.theia-button.main`);
109
+ return !!button && !button.disabled;
110
+ });
111
+ }
112
+
113
+ }
@@ -0,0 +1,73 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { TheiaDialog } from './theia-dialog';
18
+ import { TheiaView } from './theia-view';
19
+ import { containsClass } from './util';
20
+
21
+ export abstract class TheiaEditor extends TheiaView {
22
+
23
+ async isDirty(): Promise<boolean> {
24
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'theia-mod-dirty');
25
+ }
26
+
27
+ async save(): Promise<void> {
28
+ await this.activate();
29
+ if (!await this.isDirty()) {
30
+ return;
31
+ }
32
+ const fileMenu = await this.app.menuBar.openMenu('File');
33
+ const saveItem = await fileMenu.menuItemByName('Save');
34
+ await saveItem?.click();
35
+ await this.page.waitForSelector(this.tabSelector + '.theia-mod-dirty', { state: 'detached' });
36
+ }
37
+
38
+ async closeWithoutSave(): Promise<void> {
39
+ if (!await this.isDirty()) {
40
+ return super.close(true);
41
+ }
42
+ await super.close(false);
43
+ const saveDialog = new TheiaDialog(this.app);
44
+ await saveDialog.clickButton('Don\'t save');
45
+ await super.waitUntilClosed();
46
+ }
47
+
48
+ async saveAndClose(): Promise<void> {
49
+ await this.save();
50
+ await this.close();
51
+ }
52
+
53
+ async undo(times = 1): Promise<void> {
54
+ await this.activate();
55
+ for (let i = 0; i < times; i++) {
56
+ const editMenu = await this.app.menuBar.openMenu('Edit');
57
+ const undoItem = await editMenu.menuItemByName('Undo');
58
+ await undoItem?.click();
59
+ await this.app.page.waitForTimeout(200);
60
+ }
61
+ }
62
+
63
+ async redo(times = 1): Promise<void> {
64
+ await this.activate();
65
+ for (let i = 0; i < times; i++) {
66
+ const editMenu = await this.app.menuBar.openMenu('Edit');
67
+ const undoItem = await editMenu.menuItemByName('Redo');
68
+ await undoItem?.click();
69
+ await this.app.page.waitForTimeout(200);
70
+ }
71
+ }
72
+
73
+ }