diff --git a/.vscode/launch.json b/.vscode/launch.json
index d56b559990628..0c7d8dfa02df8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -176,6 +176,21 @@
"outFiles": [
"${workspaceFolder}/../.js"
]
+ },
+ {
+ "name": "Debug selected system test file with Playwright",
+ "type": "node",
+ "request": "launch",
+ "program": "${workspaceFolder}/examples/playwright/node_modules/.bin/playwright",
+ "args": [
+ "test",
+ "--debug",
+ "--config=./configs/playwright.debug.config.ts",
+ "${fileBasenameNoExtension}"
+ ],
+ "cwd": "${workspaceFolder}/examples/playwright",
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen"
}
],
"compounds": [
diff --git a/examples/playwright/.eslintrc.js b/examples/playwright/.eslintrc.js
new file mode 100644
index 0000000000000..ac7b2858d2708
--- /dev/null
+++ b/examples/playwright/.eslintrc.js
@@ -0,0 +1,12 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: [
+ '../../configs/build.eslintrc.json',
+ './configs/ui-tests.eslintrc.json',
+ './configs/ui-tests.playwright.eslintrc.json'
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: 'tsconfig.json'
+ }
+};
diff --git a/examples/playwright/.gitignore b/examples/playwright/.gitignore
new file mode 100644
index 0000000000000..465efbb8aedc8
--- /dev/null
+++ b/examples/playwright/.gitignore
@@ -0,0 +1,2 @@
+allure-results
+test-results
diff --git a/examples/playwright/README.md b/examples/playwright/README.md
new file mode 100644
index 0000000000000..85104e3b6255e
--- /dev/null
+++ b/examples/playwright/README.md
@@ -0,0 +1,28 @@
+# Theia 🎠Playwright: A System Testing Framework for Theia
+
+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.
+
+
+
+![Theia System Testing in Action](./docs/images/teaser.gif)
+
+
+
+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.
+Ready for an [example](./docs/GETTING_STARTED.md)?
+
+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.
+Check out [Playwright's documentation](https://playwright.dev/docs/intro) for more information.
+
+This page object framework not only covers Theia's generic capabilities, such as handling views, the quick command palette, file explorer etc.
+It is [extensible](./docs/EXTENSIBILITY.md) so you can add dedicated page objects for custom Theia components, such as custom views, editors, menus, etc.
+
+## Documentation
+
+- [Getting Started](./docs/GETTING_STARTED.md)
+- [Extensibility](./docs/EXTENSIBILITY.md)
+- [Building and Developing Theia 🎠Playwright](./docs/DEVELOPING.md)
+
+## Get in touch
+
+If you have problems, find bugs or have questions, feel free to get in contact via the bugs and discussions.
diff --git a/examples/playwright/configs/playwright.config.ts b/examples/playwright/configs/playwright.config.ts
new file mode 100644
index 0000000000000..1e9ef566d64ac
--- /dev/null
+++ b/examples/playwright/configs/playwright.config.ts
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { PlaywrightTestConfig } from '@playwright/test';
+
+const config: PlaywrightTestConfig = {
+ testDir: '../lib/tests',
+ testMatch: ['**/*.js'],
+ workers: 2,
+ // Timeout for each test in milliseconds.
+ timeout: 60 * 1000,
+ use: {
+ baseURL: 'http://localhost:3000',
+ browserName: 'chromium',
+ screenshot: 'only-on-failure',
+ viewport: { width: 1920, height: 1080 }
+ },
+ snapshotDir: '../tests/snapshots',
+ expect: {
+ toMatchSnapshot: { threshold: 0.15 }
+ },
+ preserveOutput: 'failures-only',
+ reporter: [
+ ['list'],
+ ['allure-playwright']
+ ]
+};
+
+export default config;
diff --git a/examples/playwright/configs/playwright.debug.config.ts b/examples/playwright/configs/playwright.debug.config.ts
new file mode 100644
index 0000000000000..91cd8e71c19b9
--- /dev/null
+++ b/examples/playwright/configs/playwright.debug.config.ts
@@ -0,0 +1,27 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { PlaywrightTestConfig } from '@playwright/test';
+
+import baseConfig from './playwright.config';
+
+const debugConfig: PlaywrightTestConfig = {
+ ...baseConfig,
+ workers: 1,
+ timeout: 15000000
+};
+
+export default debugConfig;
diff --git a/examples/playwright/configs/playwright.headful.config.ts b/examples/playwright/configs/playwright.headful.config.ts
new file mode 100644
index 0000000000000..5cd0dcf34c2b0
--- /dev/null
+++ b/examples/playwright/configs/playwright.headful.config.ts
@@ -0,0 +1,30 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { PlaywrightTestConfig } from '@playwright/test';
+
+import baseConfig from './playwright.config';
+
+const headfulConfig: PlaywrightTestConfig = {
+ ...baseConfig,
+ workers: 1,
+ use: {
+ ...baseConfig.use,
+ headless: false
+ }
+};
+
+export default headfulConfig;
diff --git a/examples/playwright/configs/ui-tests.eslintrc.json b/examples/playwright/configs/ui-tests.eslintrc.json
new file mode 100644
index 0000000000000..735abed1c450a
--- /dev/null
+++ b/examples/playwright/configs/ui-tests.eslintrc.json
@@ -0,0 +1,7 @@
+{
+ // override existing rules for ui-tests package
+ "rules": {
+ "no-undef": "off", // disabled due to 'browser', '$', '$$'
+ "no-unused-expressions": "off"
+ }
+}
diff --git a/examples/playwright/configs/ui-tests.playwright.eslintrc.json b/examples/playwright/configs/ui-tests.playwright.eslintrc.json
new file mode 100644
index 0000000000000..15f12466adae8
--- /dev/null
+++ b/examples/playwright/configs/ui-tests.playwright.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ // override existing rules for ui-tests playwright package
+ "rules": {
+ "no-null/no-null": "off"
+ }
+}
diff --git a/examples/playwright/docs/DEVELOPING.md b/examples/playwright/docs/DEVELOPING.md
new file mode 100644
index 0000000000000..1de7dc4d3ed21
--- /dev/null
+++ b/examples/playwright/docs/DEVELOPING.md
@@ -0,0 +1,26 @@
+# Building and developing Theia 🎠Playwright
+
+## Building
+
+Run `yarn` in the root directory of the repository.
+
+## Executing the tests
+
+### Prerequisites
+
+To work with the tests the Theia Application under test needs to be running.
+
+Run `yarn browser start` to start the browser-app located in this repository.
+
+You may also use the `Launch Browser Backend` launch configuration in VS Code.
+
+### Running the tests headless
+
+To start the tests run `yarn ui-tests` in the root of this repository. This will start the tests located in `tests` in a headless mode.
+
+To only run a single test file, the path of a test file can be set with `yarn ui-tests ` or `yarn ui-tests -g ""`.
+See the [Playwright Test command line documentation](https://playwright.dev/docs/intro#command-line).
+
+### Debugging the tests
+
+Please refer to the section [debugging tests](./GETTING_STARTED.md#debugging-the-tests).
diff --git a/examples/playwright/docs/EXTENSIBILITY.md b/examples/playwright/docs/EXTENSIBILITY.md
new file mode 100644
index 0000000000000..b8fabc94d4583
--- /dev/null
+++ b/examples/playwright/docs/EXTENSIBILITY.md
@@ -0,0 +1,103 @@
+# Extensibility
+
+Theia is an extensible tool platform for building custom tools with custom user interface elements, such as views, editors, commands, etc.
+Correspondingly, Theia 🎠Playwright supports adding dedicated page objects for your custom user interface elements.
+Depending on the nature of your custom components, you can extend the generic base objects, such as for views or editors, or add your own from scratch.
+
+## Custom commands or menu items
+
+Commands and menu items are handled by their label, so no further customization of the page object framework is required.
+Simply interact with them via the menu or quick commands.
+
+```typescript
+const app = await TheiaApp.load(page);
+const menuBar = app.menuBar;
+
+const yourMenu = await menuBar.openMenu("Your Menu");
+const yourItem = await mainMenu.menuItemByName("Your Item");
+
+expect(await yourItem?.hasSubmenu()).toBe(true);
+```
+
+## Custom Theia applications
+
+The main entry point of the page object model is `TheiaApp`.
+To add further capabilities to it, for instance a custom toolbar, extend the `TheiaApp` class and add an accessor for a custom toolbar page object.
+
+```typescript
+export class MyTheiaApp extends TheiaApp {
+ readonly toolbar = new MyToolbar(this);
+}
+
+export class MyToolbar extends TheiaPageObject {
+ selector = "div#myToolbar";
+ async clickItem1(): Promise {
+ await this.page.click(`${this.selector} .item1`);
+ }
+}
+
+const ws = new TheiaWorkspace(["tests/resources/sample-files1"]);
+const app = await MyTheiaApp.load(page, ws);
+await app.toolbar.clickItem1();
+```
+
+## Custom views and status indicators
+
+Many custom Theia applications add dedicated views, editors, or status indicators.
+To support these custom user interface elements in the testing framework, you can add dedicated page objects for them.
+Typically these dedicated page objects for your custom user interface elements are subclasses of the generic classes, `TheiaView`, `TheiaEditor`, etc.
+Consequently, they inherit the generic behavior of views or editors, such as activating or closing them, querying the title, check whether editors are dirty, etc.
+
+Let's take a custom view as an example. This custom view has a button that we want to be able to click.
+
+```typescript
+export class MyView extends TheiaView {
+ constructor(public app: TheiaApp) {
+ super(
+ {
+ tabSelector: "#shell-tab-my-view", // the id of the tab
+ viewSelector: "#my-view-container", // the id of the view container
+ viewName: "My View", // the user visible view name
+ },
+ app
+ );
+ }
+
+ async clickMyButton(): Promise {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ const button = await viewElement?.waitForSelector("#idOfMyButton");
+ await button?.click();
+ }
+}
+```
+
+So first, we create a new class that inherits all generic view capabilities from `TheiaView`.
+We have to specify the selectors for the tab and for the view container element that we specify in the view implementation.
+Optionally we can specify a view name, which corresponds to the label in Theia's view menu.
+This information is enough to open, close, find and interact with the view.
+
+Additionally we can add further custom methods for the specific actions and queries we want to use for our custom view.
+As an example, `MyView` above introduces a method that allows to click a button.
+
+To use this custom page object in a test, we pass our custom page object as a parameter when opening the view with `app.openView`.
+
+```typescript
+const app = await TheiaApp.load(page, ws);
+const myView = await app.openView(MyView);
+await myView.clickMyButton();
+```
+
+A similar approach is used for custom editors. The only diference is that we extend `TheiaEditor` instead and pass our custom page object as an argument to `app.openEditor`.
+As a reference for custom views and editors, please refer to the existing page objects, such as `TheiaPreferenceView`, `TheiaTextEditor`, etc.
+
+Custom status indicators are supported with the same mechanism. They are accessed via `TheiaApp.statusBar`.
+
+```typescript
+const app = await TheiaApp.load(page);
+const problemIndicator = await app.statusBar.statusIndicator(
+ TheiaProblemIndicator
+);
+const numberOfProblems = await problemIndicator.numberOfProblems();
+expect(numberOfProblems).to.be(2);
+```
diff --git a/examples/playwright/docs/GETTING_STARTED.md b/examples/playwright/docs/GETTING_STARTED.md
new file mode 100644
index 0000000000000..0fa08c98b9326
--- /dev/null
+++ b/examples/playwright/docs/GETTING_STARTED.md
@@ -0,0 +1,140 @@
+# Getting Started
+
+This repository contains several tests based on Theia 🎠Playwright in `examples/playwright/tests` that should help getting started quickly.
+You can also use this package as a template for your own tests.
+
+Please note that Theia 🎠Playwright is built to be extended with custom page objects.
+Please refer to the [extension guide](EXTENSIBILITY.md).
+
+## Adding further tests
+
+Let's write a system test for the Theia text editor as an example:
+
+1. Initialize a prepared workspace containing a file `sampleFolder/sample.txt` and open the workspace with the Theia application under test
+2. Open the Theia text editor
+3. Replace the contents of line 1 with `change` and check the line contents and the dirty state, which now should indicate that the editor is dirty.
+4. Perform an undo twice and verify that the line contents should be what it was before the change. The dirty state should be clean again.
+5. Run redo twice and check that line 1 shows the text `change` again. Also the dirty state should be changed again.
+6. Save the editor with the saved contents and check whether the editor state is clean after save. Close the editor.
+7. Reopen the same file and check whether the contents of line 1 shows still the changed contents.
+
+The test code could look as follows. We use the page objects `TheiaWorkspace` and `TheiaApp` to initialize the application with a prepared workspace.
+Using the `TheiaApp` instance, we open an editor of type `TheiaTextEditor`, which allows us to exercise actions, such as replacing line contents, undo, redo, etc.
+At any time, we can also get information from the text editor, such as obtaining dirty state and verify whether this information is what we expect.
+
+```typescript
+test("should undo and redo text changes and correctly update the dirty state", async () => {
+ // 1. set up workspace contents and open Theia app
+ const ws = new TheiaWorkspace(["tests/resources/sample-files1"]);
+ const app = await TheiaApp.load(page, ws);
+
+ // 2. open Theia text editor
+ const sampleTextEditor = await app.openEditor(
+ "sample.txt",
+ TheiaTextEditor
+ );
+
+ // 3. make a change and verify contents and dirty
+ await sampleTextEditor.replaceLineWithLineNumber("change", 1);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
+ "change"
+ );
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ // 4. undo and verify contents and dirty state
+ await sampleTextEditor.undo(2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
+ "this is just a sample file"
+ );
+ expect(await sampleTextEditor.isDirty()).toBe(false);
+
+ // 5. undo and verify contents and dirty state
+ await sampleTextEditor.redo(2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
+ "change"
+ );
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ // 6. save verify dirty state
+ await sampleTextEditor.save();
+ expect(await sampleTextEditor.isDirty()).toBe(false);
+ await sampleTextEditor.close();
+
+ // 7. reopen editor and verify dirty state
+ const reopenedEditor = await app.openEditor("sample.txt", TheiaTextEditor);
+ expect(await reopenedEditor.textContentOfLineByLineNumber(1)).toBe(
+ "change"
+ );
+
+ await reopenedEditor.close();
+});
+```
+
+Below you can see this example test in action by stepping through the code with the VSCode debug tools.
+
+
+
+![Theia](./images/debug-example.gif)
+
+
+
+## Best practices
+
+The playwright tests/functions are all asynchronous so the `await` keyword should be used to wait for the result of a given function.
+This way there is no need to use timeouts or other functions to wait for a specific result.
+As long as await is used on any call, the tests should be in the intended state.
+
+There are two ways to query the page for elements using a selector.
+
+1. One is the `page.$(selector)` method, which returns null or the element, if it is available.
+2. The other method `page.waitForSelector(selector)`, like indicated by the name, waits until the selector becomes available. This ensures that the element could be loaded and therefore negates the
+ need for null checks afterwards.
+
+Avoid directly interacting with the document object model, such as HTML elements, or the Playwright `page` from tests directly.
+The interaction with the application should be encapsulated in the page objects.
+Otherwise, your tests will get bloated and more fragile to changes of the application.
+For more information, refer to the [page object pattern](https://martinfowler.com/bliki/PageObject.html).
+
+Avoid returning or exposing Playwright types from page objects.
+This keeps your tests independent of the underlying browser automation framework.
+
+## Executing tests
+
+Before running your tests, the Theia application under test needs to be running.
+This repository already provides an example Theia application, however, you might want to test your custom Theia-based application instead.
+
+To run the application provided in this repository, run `yarn browser start` in the root of this repository after building everything with `yarn`.
+You may also use the `Launch Browser Backend` launch configuration in VS Code.
+
+### Running the tests headless
+
+To start the tests run `yarn ui-tests` in the folder `playwright`.
+This will start the tests in a headless state.
+
+To only run a single test file, the path of a test file can be set with `yarn ui-tests ` or `yarn ui-tests -g ""`.
+See the [Playwright Test command line documentation](https://playwright.dev/docs/intro#command-line).
+
+### Running the tests headfull
+
+If you want to observe the execution of the tests in a browser, use `yarn ui-tests-headful` for all tests or `yarn ui-tests-headful ` to only run a specific test.
+
+### Debugging the tests
+
+To debug a test, open the test file in the code editor and run the `Debug selected system test file with Playwright` configuration inside VS Code.
+This will start the Playwright inspector and debug the currently opened test file.
+Using this approach, you will be able to observe the tests in the browser and set breakpoints in VSCode.
+
+_Note that the tests need to be started in the playwright inspector again, as playwright pauses once the test reaches the `page.goto()` call._
+
+The Playwright inspector contains an editor that shows the currently executed code and offers debugging capabilities, which can be used instead of attaching the VS code debugger.
+
+The launched browser instance contains some additional features that are useful to debugging playwrigth tests. The browsers console contains a playwright object, which can be used to test the playwright API.
+For example the result of a given selector can be inspected. Additionally, this browser instance offers some quality of life improvements, such as preventing to resize the web application when the developer tools are opened, etc.
+
+For more information on debugging, please refer to the [Playwright documentation](https://playwright.dev/docs/debug).
+
+## Advanced Topics
+
+There are many more features, configuration and command line options from Playwright that can be used.
+These range from grouping and annotating tests, further reporters, to visual comparisons, etc.
+For more information refer to the [Playwright documentation](https://playwright.dev/docs/intro).
diff --git a/examples/playwright/docs/images/debug-example.gif b/examples/playwright/docs/images/debug-example.gif
new file mode 100644
index 0000000000000..13b6d5c25d86d
Binary files /dev/null and b/examples/playwright/docs/images/debug-example.gif differ
diff --git a/examples/playwright/docs/images/teaser.gif b/examples/playwright/docs/images/teaser.gif
new file mode 100644
index 0000000000000..5c7a555961795
Binary files /dev/null and b/examples/playwright/docs/images/teaser.gif differ
diff --git a/examples/playwright/package.json b/examples/playwright/package.json
new file mode 100644
index 0000000000000..6136a3b1bc433
--- /dev/null
+++ b/examples/playwright/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@theia/playwright-testframework",
+ "version": "0.9.0",
+ "description": "System tests for Theia",
+ "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eclipse-theia/theia.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eclipse-theia/theia/issues"
+ },
+ "homepage": "https://github.com/eclipse-theia/theia",
+ "scripts": {
+ "prepare": "yarn clean && yarn build",
+ "clean": "rimraf lib",
+ "build": "tsc --incremental && yarn lint && npx playwright install chromium",
+ "theia:start": "yarn --cwd ../browser start",
+ "lint": "eslint -c ./.eslintrc.js --ext .ts ./src ./tests",
+ "lint:fix": "eslint -c ./.eslintrc.js --ext .ts ./src ./tests --fix",
+ "ui-tests": "yarn && playwright test --config=./configs/playwright.config.ts",
+ "ui-tests-headful": "yarn && playwright test --config=./configs/playwright.headful.config.ts",
+ "ui-tests-report-generate": "allure generate ./allure-results --clean -o allure-results/allure-report",
+ "ui-tests-report": "yarn ui-tests-report-generate && allure open allure-results/allure-report"
+ },
+ "files": [
+ "src"
+ ],
+ "dependencies": {
+ "@playwright/test": "1.17.1",
+ "fs-extra": "^9.0.8"
+ },
+ "devDependencies": {
+ "@types/fs-extra": "^9.0.8",
+ "allure-commandline": "^2.13.8",
+ "allure-playwright": "^2.0.0-beta.14"
+ }
+}
diff --git a/examples/playwright/src/theia-about-dialog.ts b/examples/playwright/src/theia-about-dialog.ts
new file mode 100644
index 0000000000000..04042c7356236
--- /dev/null
+++ b/examples/playwright/src/theia-about-dialog.ts
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaDialog } from './theia-dialog';
+
+export class TheiaAboutDialog extends TheiaDialog {
+
+ async isVisible(): Promise {
+ const dialog = await this.page.$(`${this.blockSelector} .theia-aboutDialog`);
+ return !!dialog && dialog.isVisible();
+ }
+
+}
diff --git a/examples/playwright/src/theia-app.ts b/examples/playwright/src/theia-app.ts
new file mode 100644
index 0000000000000..290ed3aa45277
--- /dev/null
+++ b/examples/playwright/src/theia-app.ts
@@ -0,0 +1,136 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { Page } from '@playwright/test';
+import { TheiaEditor } from './theia-editor';
+import { DOT_FILES_FILTER, TheiaExplorerView } from './theia-explorer-view';
+import { TheiaMenuBar } from './theia-main-menu';
+import { TheiaPreferenceScope, TheiaPreferenceView } from './theia-preference-view';
+import { TheiaQuickCommandPalette } from './theia-quick-command-palette';
+import { TheiaStatusBar } from './theia-status-bar';
+import { TheiaView } from './theia-view';
+import { TheiaWorkspace } from './theia-workspace';
+
+export class TheiaApp {
+
+ readonly statusBar = new TheiaStatusBar(this);
+ readonly quickCommandPalette = new TheiaQuickCommandPalette(this);
+ readonly menuBar = new TheiaMenuBar(this);
+ public workspace: TheiaWorkspace;
+
+ public static async load(page: Page, initialWorkspace?: TheiaWorkspace): Promise {
+ const app = new TheiaApp(page, initialWorkspace);
+ await TheiaApp.loadOrReload(page, '/#' + app.workspace.urlEncodedPath);
+ await page.waitForSelector('.theia-preload', { state: 'detached' });
+ await page.waitForSelector('.theia-ApplicationShell');
+ await app.waitForInitialized();
+ return Promise.resolve(app);
+ }
+
+ protected static async loadOrReload(page: Page, url: string): Promise {
+ if (page.url() === url) {
+ await page.reload();
+ } else {
+ const wasLoadedAlready = await page.isVisible('.theia-ApplicationShell');
+ await page.goto(url);
+ if (wasLoadedAlready) {
+ // Theia doesn't refresh on URL change only
+ // So we need to reload if the app was already loaded before
+ await page.reload();
+ }
+ }
+ }
+
+ protected constructor(public page: Page, initialWorkspace?: TheiaWorkspace) {
+ this.workspace = initialWorkspace ? initialWorkspace : new TheiaWorkspace();
+ this.workspace.initialize();
+ }
+
+ async isMainContentPanelVisible(): Promise {
+ const contentPanel = await this.page.$('#theia-main-content-panel');
+ return !!contentPanel && contentPanel.isVisible();
+ }
+
+ async openPreferences(viewFactory: { new(app: TheiaApp): TheiaPreferenceView }, preferenceScope = TheiaPreferenceScope.Workspace): Promise {
+ const view = new viewFactory(this);
+ if (await view.isTabVisible()) {
+ await view.activate();
+ return view;
+ }
+ await view.open(preferenceScope);
+ return view;
+ }
+
+ async openView(viewFactory: { new(app: TheiaApp): T }): Promise {
+ const view = new viewFactory(this);
+ if (await view.isTabVisible()) {
+ await view.activate();
+ return view;
+ }
+ await view.open();
+ return view;
+ }
+
+ async openEditor(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T },
+ editorName?: string, expectFileNodes = true): Promise {
+ const explorer = await this.openView(TheiaExplorerView);
+ if (!explorer) {
+ throw Error('TheiaExplorerView could not be opened.');
+ }
+ if (expectFileNodes) {
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
+ if (fileStatElements.length < 1) {
+ throw Error('TheiaExplorerView is empty.');
+ }
+ }
+ const fileNode = await explorer.fileStatNode(filePath);
+ if (!fileNode || ! await fileNode?.isFile()) {
+ throw Error(`Specified path '${filePath}' could not be found or isn't a file.`);
+ }
+
+ const editor = new editorFactory(filePath, this);
+ const contextMenu = await fileNode.openContextMenu();
+ const editorToUse = editorName ? editorName : editor.name ? editor.name : undefined;
+ if (editorToUse) {
+ const menuItem = await contextMenu.menuItemByNamePath('Open With', editorToUse);
+ if (!menuItem) {
+ throw Error(`Editor named '${editorName}' could not be found in "Open With" menu.`);
+ }
+ await menuItem.click();
+ } else {
+ await contextMenu.clickMenuItem('Open');
+ }
+
+ await editor.waitForVisible();
+ return editor;
+ }
+
+ async activateExistingEditor(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T }): Promise {
+ const editor = new editorFactory(filePath, this);
+ if (!await editor.isTabVisible()) {
+ throw new Error(`Could not find opened editor for file ${filePath}`);
+ }
+ await editor.activate();
+ await editor.waitForVisible();
+ return editor;
+ }
+
+ /** Specific Theia apps may add additional conditions to wait for. */
+ async waitForInitialized(): Promise {
+ // empty by default
+ }
+
+}
diff --git a/examples/playwright/src/theia-context-menu.ts b/examples/playwright/src/theia-context-menu.ts
new file mode 100644
index 0000000000000..94e24f356b58f
--- /dev/null
+++ b/examples/playwright/src/theia-context-menu.ts
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+import { TheiaMenu } from './theia-menu';
+
+export class TheiaContextMenu extends TheiaMenu {
+
+ public static async openAt(app: TheiaApp, x: number, y: number): Promise {
+ await app.page.mouse.move(x, y);
+ await app.page.mouse.click(x, y, { button: 'right' });
+ return TheiaContextMenu.returnWhenVisible(app);
+ }
+
+ public static async open(app: TheiaApp, element: () => Promise>): Promise {
+ const elementHandle = await element();
+ await elementHandle.click({ button: 'right' });
+ return TheiaContextMenu.returnWhenVisible(app);
+ }
+
+ private static async returnWhenVisible(app: TheiaApp): Promise {
+ const menu = new TheiaContextMenu(app);
+ await menu.waitForVisible();
+ return menu;
+ }
+
+}
diff --git a/examples/playwright/src/theia-dialog.ts b/examples/playwright/src/theia-dialog.ts
new file mode 100644
index 0000000000000..419521aa874ab
--- /dev/null
+++ b/examples/playwright/src/theia-dialog.ts
@@ -0,0 +1,113 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaPageObject } from './theia-page-object';
+
+export class TheiaDialog extends TheiaPageObject {
+
+ protected overlaySelector = '#theia-dialog-shell';
+ protected blockSelector = this.overlaySelector + ' .dialogBlock';
+ protected titleBarSelector = this.blockSelector + ' .dialogTitle';
+ protected titleSelector = this.titleBarSelector + ' > div';
+ protected contentSelector = this.blockSelector + ' .dialogContent > div';
+ protected controlSelector = this.blockSelector + ' .dialogControl';
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(`${this.blockSelector}`, { state: 'visible' });
+ }
+
+ async waitForClosed(): Promise {
+ await this.page.waitForSelector(`${this.blockSelector}`, { state: 'detached' });
+ }
+
+ async isVisible(): Promise {
+ const pouDialogElement = await this.page.$(this.blockSelector);
+ return pouDialogElement ? pouDialogElement.isVisible() : false;
+ }
+
+ async title(): Promise {
+ const titleElement = await this.page.waitForSelector(`${this.titleSelector}`);
+ return titleElement.textContent();
+ }
+
+ async waitUntilTitleIsDisplayed(title: string): Promise {
+ await this.page.waitForFunction(predicate => {
+ const element = document.querySelector(predicate.titleSelector);
+ return !!element && element.textContent === predicate.expectedTitle;
+ }, { titleSelector: this.titleSelector, expectedTitle: title });
+ }
+
+ protected async contentElement(): Promise> {
+ return this.page.waitForSelector(this.contentSelector);
+ }
+
+ protected async buttonElement(label: string): Promise> {
+ return this.page.waitForSelector(`${this.controlSelector} button:has-text("${label}")`);
+ }
+
+ protected async buttonElementByClass(buttonClass: string): Promise> {
+ return this.page.waitForSelector(`${this.controlSelector} button${buttonClass}`);
+ }
+
+ protected async validationElement(): Promise> {
+ return this.page.waitForSelector(`${this.controlSelector} div.error`);
+ }
+
+ async getValidationText(): Promise {
+ const element = await this.validationElement();
+ return element.textContent();
+ }
+
+ async validationResult(): Promise {
+ const validationText = await this.getValidationText();
+ return validationText !== '' ? false : true;
+ }
+
+ async close(): Promise {
+ const closeButton = await this.page.waitForSelector(`${this.titleBarSelector} i.closeButton`);
+ await closeButton.click();
+ await this.waitForClosed();
+ }
+
+ async clickButton(buttonLabel: string): Promise {
+ const buttonElement = await this.buttonElement(buttonLabel);
+ await buttonElement.click();
+ }
+
+ async isButtonDisabled(buttonLabel: string): Promise {
+ const buttonElement = await this.buttonElement(buttonLabel);
+ return buttonElement.isDisabled();
+ }
+
+ async clickMainButton(): Promise {
+ const buttonElement = await this.buttonElementByClass('.theia-button.main');
+ await buttonElement.click();
+ }
+
+ async clickSecondaryButton(): Promise {
+ const buttonElement = await this.buttonElementByClass('.theia-button.secondary');
+ await buttonElement.click();
+ }
+
+ async waitUntilMainButtonIsEnabled(): Promise {
+ await this.page.waitForFunction(() => {
+ const button = document.querySelector(`${this.controlSelector} > button.theia-button.main`);
+ return !!button && !button.disabled;
+ });
+ }
+
+}
diff --git a/examples/playwright/src/theia-editor.ts b/examples/playwright/src/theia-editor.ts
new file mode 100644
index 0000000000000..520c0f44f2caa
--- /dev/null
+++ b/examples/playwright/src/theia-editor.ts
@@ -0,0 +1,73 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaDialog } from './theia-dialog';
+import { TheiaView } from './theia-view';
+import { containsClass } from './util';
+
+export abstract class TheiaEditor extends TheiaView {
+
+ async isDirty(): Promise {
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'theia-mod-dirty');
+ }
+
+ async save(): Promise {
+ await this.activate();
+ if (!await this.isDirty()) {
+ return;
+ }
+ const fileMenu = await this.app.menuBar.openMenu('File');
+ const saveItem = await fileMenu.menuItemByName('Save');
+ await saveItem?.click();
+ await this.page.waitForSelector(this.tabSelector + '.theia-mod-dirty', { state: 'detached' });
+ }
+
+ async closeWithoutSave(): Promise {
+ if (!await this.isDirty()) {
+ return super.close(true);
+ }
+ await super.close(false);
+ const saveDialog = new TheiaDialog(this.app);
+ await saveDialog.clickButton('Don\'t save');
+ await super.waitUntilClosed();
+ }
+
+ async saveAndClose(): Promise {
+ await this.save();
+ await this.close();
+ }
+
+ async undo(times = 1): Promise {
+ await this.activate();
+ for (let i = 0; i < times; i++) {
+ const editMenu = await this.app.menuBar.openMenu('Edit');
+ const undoItem = await editMenu.menuItemByName('Undo');
+ await undoItem?.click();
+ await this.app.page.waitForTimeout(200);
+ }
+ }
+
+ async redo(times = 1): Promise {
+ await this.activate();
+ for (let i = 0; i < times; i++) {
+ const editMenu = await this.app.menuBar.openMenu('Edit');
+ const undoItem = await editMenu.menuItemByName('Redo');
+ await undoItem?.click();
+ await this.app.page.waitForTimeout(200);
+ }
+ }
+
+}
diff --git a/examples/playwright/src/theia-explorer-view.ts b/examples/playwright/src/theia-explorer-view.ts
new file mode 100644
index 0000000000000..770b4ec5a9058
--- /dev/null
+++ b/examples/playwright/src/theia-explorer-view.ts
@@ -0,0 +1,229 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaApp } from './theia-app';
+import { TheiaDialog } from './theia-dialog';
+import { TheiaMenuItem } from './theia-menu-item';
+import { TheiaRenameDialog } from './theia-rename-dialog';
+import { TheiaTreeNode } from './theia-tree-node';
+import { TheiaView } from './theia-view';
+import { elementContainsClass, normalizeId } from './util';
+
+const TheiaExplorerViewData = {
+ tabSelector: '#shell-tab-explorer-view-container',
+ viewSelector: '#explorer-view-container--files',
+ viewName: 'Explorer'
+};
+
+export class TheiaExplorerFileStatNode extends TheiaTreeNode {
+
+ constructor(protected elementHandle: ElementHandle, protected explorerView: TheiaExplorerView) {
+ super(elementHandle, explorerView.app);
+ }
+
+ async absolutePath(): Promise {
+ return this.elementHandle.getAttribute('title');
+ }
+
+ async isFile(): Promise {
+ return ! await this.isFolder();
+ }
+
+ async isFolder(): Promise {
+ return elementContainsClass(this.elementHandle, 'theia-DirNode');
+ }
+
+ async getMenuItemByNamePath(...names: string[]): Promise {
+ const contextMenu = await this.openContextMenu();
+ const menuItem = await contextMenu.menuItemByNamePath(...names);
+ if (!menuItem) { throw Error('MenuItem could not be retrieved by path'); }
+ return menuItem;
+ }
+
+}
+
+export type TheiaExplorerFileStatNodePredicate = (node: TheiaExplorerFileStatNode) => Promise;
+export const DOT_FILES_FILTER: TheiaExplorerFileStatNodePredicate = async node => {
+ const label = await node.label();
+ return label ? !label.startsWith('.') : true;
+};
+
+export class TheiaExplorerView extends TheiaView {
+
+ constructor(public app: TheiaApp) {
+ super(TheiaExplorerViewData, app);
+ }
+
+ async activate(): Promise {
+ await super.activate();
+ const viewElement = await this.viewElement();
+ await viewElement?.waitForSelector('.theia-TreeContainer');
+ }
+
+ async refresh(): Promise {
+ await this.clickButton('__explorer-view-container_title:navigator.refresh');
+ }
+
+ async collapseAll(): Promise {
+ await this.clickButton('__explorer-view-container_title:navigator.collapse.all');
+ }
+
+ protected async clickButton(id: string): Promise {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ const button = await viewElement?.waitForSelector(`#${normalizeId(id)}`);
+ await button?.click();
+ }
+
+ async visibleFileStatNodes(filterPredicate: TheiaExplorerFileStatNodePredicate = (_ => Promise.resolve(true))): Promise {
+ const viewElement = await this.viewElement();
+ const handles = await viewElement?.$$('.theia-FileStatNode');
+ if (handles) {
+ const nodes = handles.map(handle => new TheiaExplorerFileStatNode(handle, this));
+ const filteredNodes = [];
+ for (const node of nodes) {
+ if ((await filterPredicate(node)) === true) {
+ filteredNodes.push(node);
+ }
+ }
+ return filteredNodes;
+ }
+ return [];
+ }
+
+ async getFileStatNodeByLabel(label: string): Promise {
+ const file = await this.fileStatNode(label);
+ if (!file) { throw Error('File stat node could not be retrieved by path fragments'); }
+ return file;
+ }
+
+ async fileStatNode(filePath: string): Promise {
+ return this.fileStatNodeBySegments(...filePath.split('/'));
+ }
+
+ async fileStatNodeBySegments(...pathFragments: string[]): Promise {
+ await super.activate();
+ const viewElement = await this.viewElement();
+
+ let currentTreeNode = undefined;
+ let fragmentsSoFar = '';
+ for (let index = 0; index < pathFragments.length; index++) {
+ const fragment = pathFragments[index];
+ fragmentsSoFar += index !== 0 ? '/' : '';
+ fragmentsSoFar += fragment;
+
+ const selector = this.treeNodeSelector(fragmentsSoFar);
+ const nextTreeNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
+ if (!nextTreeNode) {
+ throw new Error(`Tree node '${selector}' not found in explorer`);
+ }
+ currentTreeNode = new TheiaExplorerFileStatNode(nextTreeNode, this);
+ if (index < pathFragments.length - 1 && await currentTreeNode.isCollapsed()) {
+ await currentTreeNode.expand();
+ }
+ }
+
+ return currentTreeNode;
+ }
+
+ async selectTreeNode(filePath: string): Promise {
+ await this.activate();
+ const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
+ if (await this.isTreeNodeSelected(filePath)) {
+ await treeNode.focus();
+ } else {
+ await treeNode.click({ modifiers: ['Control'] });
+ }
+ }
+
+ async isTreeNodeSelected(filePath: string): Promise {
+ const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
+ return elementContainsClass(treeNode, 'theia-mod-selected');
+ }
+
+ protected treeNodeSelector(filePath: string): string {
+ return `.theia-FileStatNode:has(#${normalizeId(this.treeNodeId(filePath))})`;
+ }
+
+ protected treeNodeId(filePath: string): string {
+ const workspacePath = this.app.workspace.path;
+ return `${workspacePath}:${workspacePath}/${filePath}`;
+ }
+
+ async clickContextMenuItem(file: string, path: string[]): Promise {
+ await this.activate();
+ const fileStatNode = await this.fileStatNode(file);
+ if (!fileStatNode) { throw Error('File stat node could not be retrieved by path fragments'); }
+ const menuItem = await fileStatNode.getMenuItemByNamePath(...path);
+ await menuItem.click();
+ }
+
+ protected async existsNode(path: string, isDirectory: boolean): Promise {
+ const fileStatNode = await this.fileStatNode(path);
+ if (!fileStatNode) {
+ return false;
+ }
+ if (isDirectory) {
+ if (!await fileStatNode.isFolder()) {
+ throw Error(`FileStatNode for '${path}' is not a directory!`);
+ }
+ } else {
+ if (!await fileStatNode.isFile()) {
+ throw Error(`FileStatNode for '${path}' is not a file!`);
+ }
+ }
+ return true;
+ }
+
+ async existsFileNode(path: string): Promise {
+ return this.existsNode(path, false);
+ }
+
+ async existsDirectoryNode(path: string): Promise {
+ return this.existsNode(path, true);
+ }
+
+ async getNumberOfVisibleNodes(): Promise {
+ await this.activate();
+ await this.app.quickCommandPalette.type('Refresh in Explorer');
+ await this.app.quickCommandPalette.trigger('File: Refresh in Explorer');
+ const fileStatElements = await this.visibleFileStatNodes(DOT_FILES_FILTER);
+ return fileStatElements.length;
+ }
+
+ async deleteNode(path: string, confirm = true): Promise {
+ await this.activate();
+ await this.clickContextMenuItem(path, ['Delete']);
+
+ const confirmDialog = new TheiaDialog(this.app);
+ await confirmDialog.waitForVisible();
+ confirm ? await confirmDialog.clickMainButton() : await confirmDialog.clickSecondaryButton();
+ await confirmDialog.waitForClosed();
+ }
+
+ async renameNode(path: string, newName: string, confirm = true): Promise {
+ await this.activate();
+ await this.clickContextMenuItem(path, ['Rename']);
+
+ const renameDialog = new TheiaRenameDialog(this.app);
+ await renameDialog.waitForVisible();
+ await renameDialog.enterNewName(newName);
+ confirm ? await renameDialog.confirm() : await renameDialog.close();
+ await renameDialog.waitForClosed();
+ }
+
+}
diff --git a/examples/playwright/src/theia-main-menu.ts b/examples/playwright/src/theia-main-menu.ts
new file mode 100644
index 0000000000000..f32412f5d35ea
--- /dev/null
+++ b/examples/playwright/src/theia-main-menu.ts
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaMenu } from './theia-menu';
+import { TheiaPageObject } from './theia-page-object';
+import { normalizeId, toTextContentArray } from './util';
+
+export class TheiaMainMenu extends TheiaMenu {
+ selector = '.p-Menu.p-MenuBar-menu';
+}
+
+export class TheiaMenuBar extends TheiaPageObject {
+
+ async openMenu(menuName: string): Promise {
+ const menuBarItem = await this.menuBarItem(menuName);
+ const mainMenu = new TheiaMainMenu(this.app);
+ if (await mainMenu.isOpen()) {
+ await menuBarItem?.hover();
+ } else {
+ await menuBarItem?.click();
+ }
+ mainMenu.waitForVisible();
+ return mainMenu;
+ }
+
+ async visibleMenuBarItems(): Promise {
+ const items = await this.page.$$(this.menuBarItemSelector());
+ return toTextContentArray(items);
+ }
+
+ protected menuBarItem(label = ''): Promise | null> {
+ return this.page.waitForSelector(this.menuBarItemSelector(label));
+ }
+
+ protected menuBarItemSelector(label = ''): string {
+ return `${normalizeId('#theia:menubar')} .p-MenuBar-itemLabel >> text=${label}`;
+ }
+
+}
diff --git a/examples/playwright/src/theia-menu-item.ts b/examples/playwright/src/theia-menu-item.ts
new file mode 100644
index 0000000000000..203ed46fde27b
--- /dev/null
+++ b/examples/playwright/src/theia-menu-item.ts
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { textContent } from './util';
+
+export class TheiaMenuItem {
+
+ constructor(protected element: ElementHandle) { }
+
+ protected labelElementHandle(): Promise> {
+ return this.element.waitForSelector('.p-Menu-itemLabel');
+ }
+
+ protected shortCutElementHandle(): Promise> {
+ return this.element.waitForSelector('.p-Menu-itemShortcut');
+ }
+
+ async label(): Promise {
+ return textContent(this.labelElementHandle());
+ }
+
+ async shortCut(): Promise {
+ return textContent(this.shortCutElementHandle());
+ }
+
+ async hasSubmenu(): Promise {
+ return (await this.element.getAttribute('data-type')) === 'submenu';
+ }
+
+ async isEnabled(): Promise {
+ const classAttribute = (await this.element.getAttribute('class'));
+ if (classAttribute === undefined || classAttribute === null) {
+ return false;
+ }
+ return !classAttribute.includes('p-mod-disabled');
+ }
+
+ async click(): Promise {
+ return this.element.waitForSelector('.p-Menu-itemLabel')
+ .then(labelElement => labelElement.click({ position: { x: 10, y: 10 } }));
+ }
+
+ async hover(): Promise {
+ return this.element.hover();
+ }
+
+}
diff --git a/examples/playwright/src/theia-menu.ts b/examples/playwright/src/theia-menu.ts
new file mode 100644
index 0000000000000..fbdbb1563b5c9
--- /dev/null
+++ b/examples/playwright/src/theia-menu.ts
@@ -0,0 +1,96 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaMenuItem } from './theia-menu-item';
+import { TheiaPageObject } from './theia-page-object';
+import { isDefined } from './util';
+
+export class TheiaMenu extends TheiaPageObject {
+
+ selector = '.p-Menu';
+
+ protected async menuElementHandle(): Promise | null> {
+ return this.page.$(this.selector);
+ }
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(this.selector, { state: 'visible' });
+ }
+
+ async isOpen(): Promise {
+ const menu = await this.menuElementHandle();
+ return !!menu && menu.isVisible();
+ }
+
+ async close(): Promise {
+ if (!await this.isOpen()) {
+ return;
+ }
+ await this.page.mouse.click(0, 0);
+ await this.page.waitForSelector(this.selector, { state: 'detached' });
+ }
+
+ async menuItems(): Promise {
+ const menuHandle = await this.menuElementHandle();
+ if (!menuHandle) {
+ return [];
+ }
+ const items = await menuHandle.$$('.p-Menu-content .p-Menu-item');
+ return items.map(element => new TheiaMenuItem(element));
+ }
+
+ async clickMenuItem(name: string): Promise {
+ return (await this.page.waitForSelector(this.menuItemSelector(name))).click();
+ }
+
+ async menuItemByName(name: string): Promise {
+ const menuItems = await this.menuItems();
+ for (const item of menuItems) {
+ const label = await item.label();
+ if (label === name) {
+ return item;
+ }
+ }
+ return undefined;
+ }
+
+ async menuItemByNamePath(...names: string[]): Promise {
+ let item;
+ for (let index = 0; index < names.length; index++) {
+ item = await this.page.waitForSelector(this.menuItemSelector(names[index]), { state: 'visible' });
+ await item.hover();
+ }
+
+ const menuItemHandle = await item?.$('xpath=..');
+ if (menuItemHandle) {
+ return new TheiaMenuItem(menuItemHandle);
+ }
+ return undefined;
+ }
+
+ protected menuItemSelector(label = ''): string {
+ return `.p-Menu-content .p-Menu-itemLabel >> text=${label}`;
+ }
+
+ async visibleMenuItems(): Promise {
+ const menuItems = await this.menuItems();
+ const labels = await Promise.all(menuItems.map(item => item.label()));
+ return labels.filter(isDefined);
+ }
+
+}
diff --git a/examples/playwright/src/theia-notification-indicator.ts b/examples/playwright/src/theia-notification-indicator.ts
new file mode 100644
index 0000000000000..633a9806205aa
--- /dev/null
+++ b/examples/playwright/src/theia-notification-indicator.ts
@@ -0,0 +1,49 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaStatusIndicator } from './theia-status-indicator';
+
+const NOTIFICATION_ICON = 'codicon-bell';
+const NOTIFICATION_DOT_ICON = 'codicon-bell-dot';
+const NOTIFICATION_ICONS = [NOTIFICATION_ICON, NOTIFICATION_DOT_ICON];
+
+export class TheiaNotificationIndicator extends TheiaStatusIndicator {
+
+ protected get title(): string {
+ return 'Notification';
+ }
+
+ async isVisible(): Promise {
+ return super.isVisible(NOTIFICATION_ICONS, this.title);
+ }
+
+ async hasNotifications(): Promise {
+ return super.isVisible(NOTIFICATION_DOT_ICON, this.title);
+ }
+
+ async waitForVisible(expectNotifications = false): Promise {
+ await super.waitForVisibleByIcon(expectNotifications ? NOTIFICATION_DOT_ICON : NOTIFICATION_ICON);
+ }
+
+ async toggleOverlay(): Promise {
+ const hasNotifications = await this.hasNotifications();
+ const element = await this.getElementHandleByIcon(hasNotifications ? NOTIFICATION_DOT_ICON : NOTIFICATION_ICON, this.title);
+ if (element) {
+ await element.click();
+ }
+ }
+
+}
diff --git a/examples/playwright/src/theia-notification-overlay.ts b/examples/playwright/src/theia-notification-overlay.ts
new file mode 100644
index 0000000000000..2ec6d3251ba8e
--- /dev/null
+++ b/examples/playwright/src/theia-notification-overlay.ts
@@ -0,0 +1,94 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaApp } from './theia-app';
+import { TheiaNotificationIndicator } from './theia-notification-indicator';
+import { TheiaPageObject } from './theia-page-object';
+
+export class TheiaNotificationOverlay extends TheiaPageObject {
+
+ protected readonly HEADER_NOTIFICATIONS = 'NOTIFICATIONS';
+ protected readonly HEADER_NO_NOTIFICATIONS = 'NO NEW NOTIFICATIONS';
+
+ constructor(public app: TheiaApp, protected notificationIndicator: TheiaNotificationIndicator) {
+ super(app);
+ }
+
+ protected get selector(): string {
+ return '.theia-notifications-overlay';
+ }
+
+ protected get containerSelector(): string {
+ return `${this.selector} .theia-notifications-container.theia-notification-center`;
+ }
+
+ protected get titleSelector(): string {
+ return `${this.containerSelector} .theia-notification-center-header-title`;
+ }
+
+ async isVisible(): Promise {
+ const element = await this.page.$(`${this.containerSelector}.open`);
+ return element ? element.isVisible() : false;
+ }
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(`${this.containerSelector}.open`);
+ }
+
+ async activate(): Promise {
+ if (!await this.isVisible()) {
+ await this.notificationIndicator.toggleOverlay();
+ }
+ await this.waitForVisible();
+ }
+
+ async toggle(): Promise {
+ await this.app.quickCommandPalette.type('Toggle Notifications');
+ await this.app.quickCommandPalette.trigger('Notifications: Toggle Notifications');
+ }
+
+ protected entrySelector(entryText: string): string {
+ return `${this.containerSelector} .theia-notification-message span:has-text("${entryText}")`;
+ }
+
+ async waitForEntry(entryText: string): Promise {
+ await this.activate();
+ await this.page.waitForSelector(this.entrySelector(entryText));
+ }
+
+ async waitForEntryDetached(entryText: string): Promise {
+ await this.activate();
+ await this.page.waitForSelector(this.entrySelector(entryText), { state: 'detached' });
+ }
+
+ async isEntryVisible(entryText: string): Promise {
+ await this.activate();
+ const element = await this.page.$(this.entrySelector(entryText));
+ return !!element && element.isVisible();
+ }
+
+ protected get clearAllButtonSelector(): string {
+ return this.selector + ' .theia-notification-center-header ul > li.codicon.codicon-clear-all';
+ }
+
+ async clearAllNotifications(): Promise {
+ await this.activate();
+ const element = await this.page.waitForSelector(this.clearAllButtonSelector);
+ await element.click();
+ await this.notificationIndicator.waitForVisible(false /* expectNotificiations */);
+ }
+
+}
diff --git a/examples/playwright/src/theia-page-object.ts b/examples/playwright/src/theia-page-object.ts
new file mode 100644
index 0000000000000..5a3c0de80cbe4
--- /dev/null
+++ b/examples/playwright/src/theia-page-object.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { Page } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+
+export abstract class TheiaPageObject {
+
+ constructor(public app: TheiaApp) { }
+
+ get page(): Page {
+ return this.app.page;
+ }
+
+}
diff --git a/examples/playwright/src/theia-preference-view.ts b/examples/playwright/src/theia-preference-view.ts
new file mode 100644
index 0000000000000..5ee300ce56d3a
--- /dev/null
+++ b/examples/playwright/src/theia-preference-view.ts
@@ -0,0 +1,226 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaApp } from './theia-app';
+import { TheiaView } from './theia-view';
+
+const TheiaSettingsViewData = {
+ tabSelector: '#shell-tab-settings_widget',
+ viewSelector: '#settings_widget'
+};
+
+export const PreferenceIds = {
+ Explorer: {
+ AutoReveal: 'explorer.autoReveal'
+ },
+ DiffEditor: {
+ MaxComputationTime: 'diffEditor.maxComputationTime'
+ }
+};
+
+export const DefaultPreferences = {
+ Explorer: {
+ AutoReveal: {
+ Enabled: true
+ }
+ },
+ DiffEditor: {
+ MaxComputationTime: '5000'
+ }
+};
+
+export enum TheiaPreferenceScope {
+ User = 'User',
+ Workspace = 'Workspace'
+}
+
+export class TheiaPreferenceView extends TheiaView {
+ public customTimeout?: number;
+
+ constructor(app: TheiaApp) {
+ super(TheiaSettingsViewData, app);
+ }
+
+ async open(preferenceScope = TheiaPreferenceScope.Workspace): Promise {
+ await this.app.quickCommandPalette.trigger('Preferences: Open Settings (UI)');
+ await this.waitForVisible();
+ await this.openPreferenceScope(preferenceScope);
+ return this;
+ }
+
+ protected getScopeSelector(scope: TheiaPreferenceScope): string {
+ return `li.preferences-scope-tab div.p-TabBar-tabLabel:has-text("${scope}")`;
+ }
+
+ async openPreferenceScope(scope: TheiaPreferenceScope): Promise {
+ await this.activate();
+ const scopeTab = await this.page.waitForSelector(this.getScopeSelector(scope));
+ await scopeTab.click();
+ }
+
+ async getBooleanPreferenceById(preferenceId: string): Promise {
+ const element = await this.findPreferenceEditorById(preferenceId);
+ return element.isChecked();
+ }
+
+ async getBooleanPreferenceByPath(sectionTitle: string, name: string): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.getBooleanPreferenceById(preferenceId);
+ }
+
+ async setBooleanPreferenceById(preferenceId: string, value: boolean): Promise {
+ const element = await this.findPreferenceEditorById(preferenceId);
+ return value ? element.check() : element.uncheck();
+ }
+
+ async setBooleanPreferenceByPath(sectionTitle: string, name: string, value: boolean): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.setBooleanPreferenceById(preferenceId, value);
+ }
+
+ async getStringPreferenceById(preferenceId: string): Promise {
+ const element = await this.findPreferenceEditorById(preferenceId);
+ return element.evaluate(e => (e as HTMLInputElement).value);
+ }
+
+ async getStringPreferenceByPath(sectionTitle: string, name: string): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.getStringPreferenceById(preferenceId);
+ }
+
+ async setStringPreferenceById(preferenceId: string, value: string): Promise {
+ const element = await this.findPreferenceEditorById(preferenceId);
+ return element.fill(value);
+ }
+
+ async setStringPreferenceByPath(sectionTitle: string, name: string, value: string): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.setStringPreferenceById(preferenceId, value);
+ }
+
+ async waitForModified(preferenceId: string): Promise {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}.theia-mod-item-modified`, { timeout: this.customTimeout });
+ }
+
+ async resetStringPreferenceById(preferenceId: string): Promise {
+ const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
+ if (!resetPreferenceButton) {
+ // preference not modified
+ return;
+ }
+ const previousValue = await this.getStringPreferenceById(preferenceId);
+ const selector = this.getPreferenceEditorSelector(preferenceId);
+ const done = await resetPreferenceButton.click();
+ await this.page.waitForFunction(data => {
+ const element = document.querySelector(data.selector);
+ if (!element) {
+ throw new Error(`Could not find preference element with id "${data.preferenceId}"`);
+ }
+ const value = (element as HTMLInputElement).value;
+ return value !== data.previousValue;
+ }, { preferenceId, selector, previousValue, done }, { timeout: this.customTimeout });
+ }
+
+ async resetStringPreferenceByPath(sectionTitle: string, name: string): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.resetStringPreferenceById(preferenceId);
+ }
+
+ async resetBooleanPreferenceById(preferenceId: string): Promise {
+ const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
+ if (!resetPreferenceButton) {
+ // preference not modified
+ return;
+ }
+ const previousValue = await this.getBooleanPreferenceById(preferenceId);
+ const selector = this.getPreferenceEditorSelector(preferenceId);
+ const done = await resetPreferenceButton.click();
+ await this.page.waitForFunction(data => {
+ const element = document.querySelector(data.selector);
+ if (!element) {
+ throw new Error(`Could not find preference element with id "${data.preferenceId}"`);
+ }
+ const value = (element as HTMLInputElement).checked;
+ return value !== data.previousValue;
+ }, { preferenceId, selector, previousValue, done }, { timeout: this.customTimeout });
+ }
+
+ async resetBooleanPreferenceByPath(sectionTitle: string, name: string): Promise {
+ const preferenceId = await this.findPreferenceId(sectionTitle, name);
+ return this.resetBooleanPreferenceById(preferenceId);
+ }
+
+ private async findPreferenceId(sectionTitle: string, name: string): Promise {
+ const viewElement = await this.viewElement();
+ const sectionElement = await viewElement?.$(`xpath=//li[contains(@class, 'settings-section-title') and text() = '${sectionTitle}']/..`);
+
+ const firstPreferenceAfterSection = await sectionElement?.$(`xpath=following-sibling::li[div/text() = '${name}'][1]`);
+ const preferenceId = await firstPreferenceAfterSection?.getAttribute('data-pref-id');
+ if (!preferenceId) {
+ throw new Error(`Could not find preference id for "${sectionTitle}" > (...) > "${name}"`);
+ }
+ return preferenceId;
+ }
+
+ private async findPreferenceEditorById(preferenceId: string): Promise> {
+ const viewElement = await this.viewElement();
+ const element = await viewElement?.waitForSelector(this.getPreferenceEditorSelector(preferenceId), { timeout: this.customTimeout });
+ if (!element) {
+ throw new Error(`Could not find element with preference id "${preferenceId}"`);
+ }
+ return element;
+ }
+
+ private getPreferenceSelector(preferenceId: string): string {
+ return `li[data-pref-id="${preferenceId}"]`;
+ }
+
+ private getPreferenceEditorSelector(preferenceId: string): string {
+ return `${this.getPreferenceSelector(preferenceId)} input`;
+ }
+
+ private getPreferenceGutterSelector(preferenceId: string): string {
+ return `${this.getPreferenceSelector(preferenceId)} .pref-context-gutter`;
+ }
+
+ private async findPreferenceResetButton(preferenceId: string): Promise | undefined> {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ const gutter = await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}`, { timeout: this.customTimeout });
+ if (!gutter) {
+ throw new Error(`Could not determine modified state for element with preference id "${preferenceId}"`);
+ }
+ const isModified = await gutter.evaluate(e => e.classList.contains('theia-mod-item-modified'));
+ if (!isModified) {
+ return undefined;
+ }
+
+ const settingsContextMenuBtn = await viewElement?.waitForSelector(`${this.getPreferenceSelector(preferenceId)} .settings-context-menu-btn`);
+ if (!settingsContextMenuBtn) {
+ throw new Error(`Could not find context menu button for element with preference id "${preferenceId}"`);
+ }
+ await settingsContextMenuBtn.click();
+ const resetPreferenceButton = await this.page.waitForSelector('li[data-command="preferences:reset"]');
+ if (!resetPreferenceButton) {
+ throw new Error(`Could not find menu entry to reset preference with id "${preferenceId}"`);
+ }
+ return resetPreferenceButton;
+ }
+
+}
diff --git a/examples/playwright/src/theia-problem-indicator.ts b/examples/playwright/src/theia-problem-indicator.ts
new file mode 100644
index 0000000000000..4f34d173aaeb2
--- /dev/null
+++ b/examples/playwright/src/theia-problem-indicator.ts
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaStatusIndicator } from './theia-status-indicator';
+
+const PROBLEM_ICON = 'codicon-error';
+
+export class TheiaProblemIndicator extends TheiaStatusIndicator {
+
+ async isVisible(): Promise {
+ const handle = await super.getElementHandleByIcon(PROBLEM_ICON);
+ return !!handle && handle.isVisible();
+ }
+
+ async numberOfProblems(): Promise {
+ const spans = await this.getSpans();
+ return spans ? +await spans[1].innerText() : -1;
+ }
+
+ async numberOfWarnings(): Promise {
+ const spans = await this.getSpans();
+ return spans ? +await spans[3].innerText() : -1;
+ }
+
+ protected async getSpans(): Promise {
+ const handle = await super.getElementHandleByIcon(PROBLEM_ICON);
+ return handle?.$$('span');
+ }
+
+}
diff --git a/examples/playwright/src/theia-problem-view.ts b/examples/playwright/src/theia-problem-view.ts
new file mode 100644
index 0000000000000..4916ae56dd1e7
--- /dev/null
+++ b/examples/playwright/src/theia-problem-view.ts
@@ -0,0 +1,30 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaApp } from './theia-app';
+import { TheiaView } from './theia-view';
+
+const TheiaProblemsViewData = {
+ tabSelector: '#shell-tab-problems',
+ viewSelector: '#problems',
+ viewName: 'Problems'
+};
+
+export class TheiaProblemsView extends TheiaView {
+ constructor(app: TheiaApp) {
+ super(TheiaProblemsViewData, app);
+ }
+}
diff --git a/examples/playwright/src/theia-quick-command-palette.ts b/examples/playwright/src/theia-quick-command-palette.ts
new file mode 100644
index 0000000000000..078dc1e7cecf2
--- /dev/null
+++ b/examples/playwright/src/theia-quick-command-palette.ts
@@ -0,0 +1,75 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaPageObject } from './theia-page-object';
+import { USER_KEY_TYPING_DELAY } from './util';
+
+export class TheiaQuickCommandPalette extends TheiaPageObject {
+
+ selector = '.quick-input-widget';
+
+ async open(): Promise {
+ await this.page.keyboard.press('Control+Shift+p');
+ await this.page.waitForSelector(this.selector);
+ }
+
+ async isOpen(): Promise {
+ try {
+ await this.page.waitForSelector(this.selector, { timeout: 5000 });
+ } catch (err) {
+ return false;
+ }
+ return true;
+ }
+
+ async trigger(...commandName: string[]): Promise {
+ for (const command of commandName) {
+ await this.triggerSingleCommand(command);
+ }
+ }
+
+ protected async triggerSingleCommand(commandName: string): Promise {
+ if (!await this.isOpen()) {
+ this.open();
+ }
+ let selected = await this.selectedCommand();
+ while (!(await selected?.getAttribute('aria-label') === commandName)) {
+ await this.page.keyboard.press('ArrowDown');
+ selected = await this.selectedCommand();
+ }
+ await this.page.keyboard.press('Enter');
+ }
+
+ async type(command: string): Promise {
+ if (!await this.isOpen()) {
+ this.open();
+ }
+ const input = await this.page.waitForSelector(`${this.selector} .monaco-inputbox .input`);
+ if (input != null) {
+ await input.focus();
+ await input.type(command, { delay: USER_KEY_TYPING_DELAY });
+ }
+ }
+
+ protected async selectedCommand(): Promise | null> {
+ const command = await this.page.waitForSelector(this.selector);
+ if (!command) {
+ throw new Error('No selected command found!');
+ }
+ return command.$('.monaco-list-row.focused');
+ }
+}
diff --git a/examples/playwright/src/theia-rename-dialog.ts b/examples/playwright/src/theia-rename-dialog.ts
new file mode 100644
index 0000000000000..fc62f34334a23
--- /dev/null
+++ b/examples/playwright/src/theia-rename-dialog.ts
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaDialog } from './theia-dialog';
+import { USER_KEY_TYPING_DELAY } from './util';
+
+export class TheiaRenameDialog extends TheiaDialog {
+
+ async enterNewName(newName: string): Promise {
+ const inputField = await this.page.waitForSelector(`${this.blockSelector} .theia-input`);
+ await inputField.press('Control+a');
+ await inputField.type(newName, { delay: USER_KEY_TYPING_DELAY });
+ await this.page.waitForTimeout(USER_KEY_TYPING_DELAY);
+ }
+
+ async confirm(): Promise {
+ if (!await this.validationResult()) {
+ throw new Error(`Unexpected validation error in TheiaRenameDialog: '${await this.getValidationText()}`);
+ }
+ await this.clickMainButton();
+ }
+
+}
diff --git a/examples/playwright/src/theia-status-bar.ts b/examples/playwright/src/theia-status-bar.ts
new file mode 100644
index 0000000000000..420bab402eaab
--- /dev/null
+++ b/examples/playwright/src/theia-status-bar.ts
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+import { TheiaPageObject } from './theia-page-object';
+import { TheiaStatusIndicator } from './theia-status-indicator';
+
+export class TheiaStatusBar extends TheiaPageObject {
+
+ selector = 'div#theia-statusBar';
+
+ protected async statusBarElementHandle(): Promise | null> {
+ return this.page.$(this.selector);
+ }
+
+ async statusIndicator(statusIndicatorFactory: { new(app: TheiaApp): T }): Promise {
+ return new statusIndicatorFactory(this.app);
+ }
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(this.selector, { state: 'visible' });
+ }
+
+ async isVisible(): Promise {
+ const statusBar = await this.statusBarElementHandle();
+ return !!statusBar && statusBar.isVisible();
+ }
+
+}
diff --git a/examples/playwright/src/theia-status-indicator.ts b/examples/playwright/src/theia-status-indicator.ts
new file mode 100644
index 0000000000000..637d60742e20b
--- /dev/null
+++ b/examples/playwright/src/theia-status-indicator.ts
@@ -0,0 +1,79 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+import { TheiaPageObject } from './theia-page-object';
+
+export class TheiaStatusIndicator extends TheiaPageObject {
+
+ protected elementSpanSelector = '#theia-statusBar .element span';
+
+ protected async getElementHandle(): Promise | null> {
+ return this.page.$(this.elementSpanSelector);
+ }
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(this.elementSpanSelector);
+ }
+
+ protected getSelectorByTitle(title: string): string {
+ return `.element[title="${title}"]`;
+ }
+
+ async getElementHandleByTitle(title: string): Promise | null> {
+ // Fetch element via title in case status elements exist without a dedicated Codicon icon
+ return this.page.$(this.getSelectorByTitle(title));
+ }
+
+ protected getSelectorByIcon(icon: string): string {
+ return `${this.elementSpanSelector}.codicon.${icon}`;
+ }
+
+ async getElementHandleByIcon(iconClass: string | string[], titleContain = ''): Promise | null> {
+ const icons = Array.isArray(iconClass) ? iconClass : [iconClass];
+ for (const icon of icons) {
+ const span = await this.page.$(this.getSelectorByIcon(icon));
+ if (span) {
+ const parent = await span.$('..');
+ if (titleContain === '') {
+ return parent;
+ } else {
+ const parentTitle = await parent?.getAttribute('title');
+ if (parentTitle?.includes(titleContain)) { return parent; }
+ }
+ }
+ }
+ throw new Error('Cannot find indicator');
+ }
+
+ async waitForVisibleByTitle(title: string, waitForDetached = false): Promise {
+ await this.page.waitForSelector(this.getSelectorByTitle(title), waitForDetached ? { state: 'detached' } : {});
+ }
+
+ async waitForVisibleByIcon(icon: string, waitForDetached = false): Promise {
+ await this.page.waitForSelector(this.getSelectorByIcon(icon), waitForDetached ? { state: 'detached' } : {});
+ }
+
+ async isVisible(icon: string | string[], titleContain = ''): Promise {
+ try {
+ const element = await this.getElementHandleByIcon(icon, titleContain);
+ return !!element && element.isVisible();
+ } catch (err) {
+ return false;
+ }
+ }
+
+}
diff --git a/examples/playwright/src/theia-text-editor.ts b/examples/playwright/src/theia-text-editor.ts
new file mode 100644
index 0000000000000..203fcc098ccf7
--- /dev/null
+++ b/examples/playwright/src/theia-text-editor.ts
@@ -0,0 +1,168 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+import { TheiaEditor } from './theia-editor';
+import { normalizeId } from './util';
+
+export class TheiaTextEditor extends TheiaEditor {
+
+ constructor(filePath: string, app: TheiaApp) {
+ super({
+ tabSelector: normalizeId(`#shell-tab-code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`),
+ viewSelector: normalizeId(`#code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`) + '.theia-editor'
+ }, app);
+ }
+
+ async numberOfLines(): Promise {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ const lineElements = await viewElement?.$$('.view-lines .view-line');
+ return lineElements?.length;
+ }
+
+ async textContentOfLineByLineNumber(lineNumber: number): Promise {
+ const lineElement = await this.lineByLineNumber(lineNumber);
+ const content = await lineElement?.textContent();
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
+ }
+
+ async replaceLineWithLineNumber(text: string, lineNumber: number): Promise {
+ await this.selectLineWithLineNumber(lineNumber);
+ await this.typeTextAndHitEnter(text);
+ }
+
+ protected async typeTextAndHitEnter(text: string): Promise {
+ await this.page.keyboard.type(text);
+ await this.page.keyboard.press('Enter');
+ }
+
+ async selectLineWithLineNumber(lineNumber: number): Promise | undefined> {
+ await this.activate();
+ const lineElement = await this.lineByLineNumber(lineNumber);
+ await this.selectLine(lineElement);
+ return lineElement;
+ }
+
+ async placeCursorInLineWithLineNumber(lineNumber: number): Promise | undefined> {
+ await this.activate();
+ const lineElement = await this.lineByLineNumber(lineNumber);
+ await this.placeCursorInLine(lineElement);
+ return lineElement;
+ }
+
+ async deleteLineByLineNumber(lineNumber: number): Promise {
+ await this.selectLineWithLineNumber(lineNumber);
+ await this.page.keyboard.press('Backspace');
+ }
+
+ protected async lineByLineNumber(lineNumber: number): Promise | undefined> {
+ await this.activate();
+ const viewElement = await this.viewElement();
+ const lines = await viewElement?.$$('.view-lines .view-line');
+ if (!lines) {
+ throw new Error(`Couldn't retrieve lines of text editor ${this.tabSelector}`);
+ }
+
+ const linesWithXCoordinates = [];
+ for (const lineElement of lines) {
+ const box = await lineElement.boundingBox();
+ linesWithXCoordinates.push({ x: box ? box.x : Number.MAX_VALUE, lineElement });
+ }
+ linesWithXCoordinates.sort((a, b) => a.x.toString().localeCompare(b.x.toString()));
+ return linesWithXCoordinates[lineNumber - 1].lineElement;
+ }
+
+ async textContentOfLineContainingText(text: string): Promise {
+ await this.activate();
+ const lineElement = await this.lineContainingText(text);
+ const content = await lineElement?.textContent();
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
+ }
+
+ async replaceLineContainingText(newText: string, oldText: string): Promise {
+ await this.selectLineContainingText(oldText);
+ await this.typeTextAndHitEnter(newText);
+ }
+
+ async selectLineContainingText(text: string): Promise | undefined> {
+ await this.activate();
+ const lineElement = await this.lineContainingText(text);
+ await this.selectLine(lineElement);
+ return lineElement;
+ }
+
+ async placeCursorInLineContainingText(text: string): Promise | undefined> {
+ await this.activate();
+ const lineElement = await this.lineContainingText(text);
+ await this.placeCursorInLine(lineElement);
+ return lineElement;
+ }
+
+ async deleteLineContainingText(text: string): Promise {
+ await this.selectLineContainingText(text);
+ await this.page.keyboard.press('Backspace');
+ }
+
+ async addTextToNewLineAfterLineContainingText(textContainedByExistingLine: string, newText: string): Promise {
+ const existingLine = await this.lineContainingText(textContainedByExistingLine);
+ await this.placeCursorInLine(existingLine);
+ await this.page.keyboard.press('End');
+ await this.page.keyboard.press('Enter');
+ await this.page.keyboard.type(newText);
+ }
+
+ async addTextToNewLineAfterLineByLineNumber(lineNumber: number, newText: string): Promise {
+ const existingLine = await this.lineByLineNumber(lineNumber);
+ await this.placeCursorInLine(existingLine);
+ await this.page.keyboard.press('End');
+ await this.page.keyboard.press('Enter');
+ await this.page.keyboard.type(newText);
+ }
+
+ protected async lineContainingText(text: string): Promise | undefined> {
+ const viewElement = await this.viewElement();
+ return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
+ }
+
+ protected async selectLine(lineElement: ElementHandle | undefined): Promise {
+ await lineElement?.click({ clickCount: 3 });
+ }
+
+ protected async placeCursorInLine(lineElement: ElementHandle | undefined): Promise {
+ await lineElement?.click();
+ }
+
+ protected replaceEditorSymbolsWithSpace(content: string): string | Promise {
+ // [ ] => \u00a0
+ // [·] · => \u00b7
+ return content.replace(/[\u00a0\u00b7]/g, ' ');
+ }
+
+ protected async selectedSuggestion(): Promise> {
+ return this.page.waitForSelector(this.viewSelector + ' .monaco-list-row.show-file-icons.focused');
+ }
+
+ async getSelectedSuggestionText(): Promise {
+ const suggestion = await this.selectedSuggestion();
+ const text = await suggestion.textContent();
+ if (text === null) { throw new Error('Text content could not be found'); }
+ return text;
+ }
+
+}
diff --git a/examples/playwright/src/theia-toggle-bottom-indicator.ts b/examples/playwright/src/theia-toggle-bottom-indicator.ts
new file mode 100644
index 0000000000000..5194e9244b81a
--- /dev/null
+++ b/examples/playwright/src/theia-toggle-bottom-indicator.ts
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaStatusIndicator } from './theia-status-indicator';
+
+const TOGGLE_BOTTOM_ICON = 'codicon-window';
+
+export class TheiaToggleBottomIndicator extends TheiaStatusIndicator {
+ async isVisible(): Promise {
+ return super.isVisible(TOGGLE_BOTTOM_ICON);
+ }
+}
diff --git a/examples/playwright/src/theia-tree-node.ts b/examples/playwright/src/theia-tree-node.ts
new file mode 100644
index 0000000000000..f1fb0fd136286
--- /dev/null
+++ b/examples/playwright/src/theia-tree-node.ts
@@ -0,0 +1,60 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+import { TheiaContextMenu } from './theia-context-menu';
+import { TheiaMenu } from './theia-menu';
+
+export class TheiaTreeNode {
+
+ labelElementCssClass = '.theia-TreeNodeSegmentGrow';
+ expansionToggleCssClass = '.theia-ExpansionToggle';
+ collapsedCssClass = '.theia-mod-collapsed';
+
+ constructor(protected elementHandle: ElementHandle, protected app: TheiaApp) { }
+
+ async label(): Promise {
+ const labelNode = await this.elementHandle.$(this.labelElementCssClass);
+ if (!labelNode) {
+ throw new Error('Cannot read label of ' + this.elementHandle);
+ }
+ return labelNode.textContent();
+ }
+
+ async isCollapsed(): Promise {
+ return !! await this.elementHandle.$(this.collapsedCssClass);
+ }
+
+ async isExpandable(): Promise {
+ return !! await this.elementHandle.$(this.expansionToggleCssClass);
+ }
+
+ async expand(): Promise {
+ if (! await this.isCollapsed()) {
+ return;
+ }
+ const expansionToggle = await this.elementHandle.waitForSelector(this.expansionToggleCssClass);
+ await expansionToggle.click();
+ await this.elementHandle.waitForSelector(`${this.expansionToggleCssClass}:not(${this.collapsedCssClass})`);
+ }
+
+ async openContextMenu(): Promise {
+ return TheiaContextMenu.open(this.app, () => this.elementHandle.waitForSelector(this.labelElementCssClass));
+ }
+
+}
diff --git a/examples/playwright/src/theia-view.ts b/examples/playwright/src/theia-view.ts
new file mode 100644
index 0000000000000..cd867ac41d7d5
--- /dev/null
+++ b/examples/playwright/src/theia-view.ts
@@ -0,0 +1,177 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+import { TheiaApp } from './theia-app';
+import { TheiaContextMenu } from './theia-context-menu';
+import { TheiaMenu } from './theia-menu';
+import { TheiaPageObject } from './theia-page-object';
+import { containsClass, isElementVisible, textContent } from './util';
+
+export interface TheiaViewData {
+ tabSelector: string;
+ viewSelector: string;
+ viewName?: string;
+}
+
+export class TheiaView extends TheiaPageObject {
+
+ constructor(protected readonly data: TheiaViewData, app: TheiaApp) {
+ super(app);
+ }
+
+ get tabSelector(): string {
+ return this.data.tabSelector;
+ }
+
+ get viewSelector(): string {
+ return this.data.viewSelector;
+ }
+
+ get name(): string | undefined {
+ return this.data.viewName;
+ }
+
+ async open(): Promise {
+ if (!this.data.viewName) {
+ throw new Error('View name must be specified to open via command palette');
+ }
+ await this.app.quickCommandPalette.trigger('View: Open View...', this.data.viewName);
+ await this.waitForVisible();
+ return this;
+ }
+
+ async focus(): Promise {
+ await this.activate();
+ const view = await this.viewElement();
+ await view?.click();
+ }
+
+ async activate(): Promise {
+ await this.page.waitForSelector(this.tabSelector, { state: 'visible' });
+ if (!await this.isActive()) {
+ const tab = await this.tabElement();
+ await tab?.click();
+ }
+ return this.waitForVisible();
+ }
+
+ async waitForVisible(): Promise {
+ await this.page.waitForSelector(this.viewSelector, { state: 'visible' });
+ }
+
+ async isTabVisible(): Promise {
+ return isElementVisible(this.tabElement());
+ }
+
+ async isDisplayed(): Promise {
+ return isElementVisible(this.viewElement());
+ }
+
+ async isActive(): Promise {
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'p-mod-current');
+ }
+
+ async isClosable(): Promise {
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'p-mod-closable');
+ }
+
+ async close(waitForClosed = true): Promise {
+ if (!(await this.isTabVisible())) {
+ return;
+ }
+ if (!(await this.isClosable())) {
+ throw Error(`View ${this.tabSelector} is not closable`);
+ }
+ const tab = await this.tabElement();
+ const side = await this.side();
+ if (side === 'main' || side === 'bottom') {
+ const closeIcon = await tab?.waitForSelector('div.p-TabBar-tabCloseIcon');
+ await closeIcon?.click();
+ } else {
+ const menu = await this.openContextMenuOnTab();
+ const closeItem = await menu.menuItemByName('Close');
+ await closeItem?.click();
+ }
+ if (waitForClosed) {
+ await this.waitUntilClosed();
+ }
+ }
+
+ protected async waitUntilClosed(): Promise {
+ await this.page.waitForSelector(this.tabSelector, { state: 'detached' });
+ }
+
+ async title(): Promise {
+ if ((await this.isInSidePanel()) && !(await this.isActive())) {
+ // we can only determine the label of a side-panel view, if it is active
+ await this.activate();
+ }
+ switch (await this.side()) {
+ case 'left':
+ return textContent(this.page.waitForSelector('div.theia-left-side-panel > div.theia-sidepanel-title'));
+ case 'right':
+ return textContent(this.page.waitForSelector('div.theia-right-side-panel > div.theia-sidepanel-title'));
+ }
+ const tab = await this.tabElement();
+ if (tab) {
+ return textContent(tab.waitForSelector('div.theia-tab-icon-label > div.p-TabBar-tabLabel'));
+ }
+ return undefined;
+ }
+
+ async isInSidePanel(): Promise {
+ return (await this.side() === 'left') || (await this.side() === 'right');
+ }
+
+ async side(): Promise<'left' | 'right' | 'bottom' | 'main'> {
+ if (!await this.isTabVisible()) {
+ throw Error(`Unable to determine side of invisible view tab '${this.tabSelector}'`);
+ }
+ const tab = await this.tabElement();
+ let appAreaElement = tab?.$('xpath=../..');
+ if (await containsClass(appAreaElement, 'theia-app-left')) {
+ return 'left';
+ }
+ if (await containsClass(appAreaElement, 'theia-app-right')) {
+ return 'right';
+ }
+
+ appAreaElement = (await appAreaElement)?.$('xpath=../..');
+ if (await containsClass(appAreaElement, 'theia-app-bottom')) {
+ return 'bottom';
+ }
+ if (await containsClass(appAreaElement, 'theia-app-main')) {
+ return 'main';
+ }
+ throw Error(`Unable to determine side of view tab '${this.tabSelector}'`);
+ }
+
+ async openContextMenuOnTab(): Promise {
+ await this.activate();
+ return TheiaContextMenu.open(this.app, () => this.page.waitForSelector(this.tabSelector));
+ }
+
+ protected viewElement(): Promise | null> {
+ return this.page.$(this.viewSelector);
+ }
+
+ protected tabElement(): Promise | null> {
+ return this.page.$(this.tabSelector);
+ }
+
+}
diff --git a/examples/playwright/src/theia-workspace.ts b/examples/playwright/src/theia-workspace.ts
new file mode 100644
index 0000000000000..3d1a10a99d720
--- /dev/null
+++ b/examples/playwright/src/theia-workspace.ts
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import * as fs from 'fs-extra';
+import { tmpdir } from 'os';
+import { sep } from 'path';
+import * as path from 'path';
+
+export class TheiaWorkspace {
+
+ protected workspacePath: string;
+
+ /**
+ * Creates a Theia workspace location with the specified path to files that shall be copied to this workspace.
+ * The `pathOfFilesToInitialize` must be relative to cwd of the node process.
+ *
+ * @param {string[]} pathOfFilesToInitialize Path to files or folders that shall be copied to the workspace
+ */
+ constructor(protected pathOfFilesToInitialize?: string[]) {
+ this.workspacePath = fs.mkdtempSync(`${tmpdir}${sep}cloud-ws-`);
+ }
+
+ /** Performs the file system operations preparing the workspace location synchronously. */
+ initialize(): void {
+ if (this.pathOfFilesToInitialize) {
+ for (const initPath of this.pathOfFilesToInitialize) {
+ const absoluteInitPath = path.resolve(process.cwd(), initPath);
+ if (!fs.pathExistsSync(absoluteInitPath)) {
+ throw Error('Workspace does not exist at ' + absoluteInitPath);
+ }
+ fs.copySync(absoluteInitPath, this.workspacePath);
+ }
+ }
+ }
+
+ get path(): string {
+ return this.workspacePath;
+ }
+
+ get urlEncodedPath(): string {
+ return this.path.replace(/[\\]/g, '/');
+ }
+
+ get escapedPath(): string {
+ return this.path.replace(/:/g, '%3A').replace(/[\\]/g, '%5C');
+ }
+
+ clear(): void {
+ fs.emptyDirSync(this.workspacePath);
+ }
+
+}
diff --git a/examples/playwright/src/util.ts b/examples/playwright/src/util.ts
new file mode 100644
index 0000000000000..63df3a9d7ce7f
--- /dev/null
+++ b/examples/playwright/src/util.ts
@@ -0,0 +1,72 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { ElementHandle } from '@playwright/test';
+
+export const USER_KEY_TYPING_DELAY = 80;
+
+export function normalizeId(nodeId: string): string {
+ // Special characters (i.e. in our case '.',':','/','%', and '\\') in CSS IDs have to be escaped
+ return nodeId.replace(/[.:,%/\\]/g, matchedChar => '\\' + matchedChar);
+}
+
+export async function toTextContentArray(items: ElementHandle[]): Promise {
+ const contents = items.map(item => item.textContent());
+ const resolvedContents = await Promise.all(contents);
+ return resolvedContents.filter(text => text !== undefined) as string[];
+}
+
+export function isDefined(content: string | undefined): content is string {
+ return content !== undefined;
+}
+
+export function isNotNull(content: string | null): content is string {
+ return content !== null;
+}
+
+export async function textContent(elementPromise: Promise | null>): Promise {
+ const element = await elementPromise;
+ if (!element) {
+ return undefined;
+ }
+ const content = await element.textContent();
+ return content ? content : undefined;
+}
+
+export async function containsClass(elementPromise: Promise | null> | undefined, cssClass: string): Promise {
+ return elementContainsClass(await elementPromise, cssClass);
+}
+
+export async function elementContainsClass(element: ElementHandle | null | undefined, cssClass: string): Promise {
+ if (element) {
+ const classValue = await element.getAttribute('class');
+ if (classValue) {
+ return classValue?.split(' ').includes(cssClass);
+ }
+ }
+ return false;
+}
+
+export async function isElementVisible(elementPromise: Promise | null>): Promise {
+ const element = await elementPromise;
+ return element ? element.isVisible() : false;
+}
+
+export async function elementId(element: ElementHandle): Promise {
+ const id = await element.getAttribute('id');
+ if (id === null) { throw new Error('Could not get ID of ' + element); }
+ return id;
+}
diff --git a/examples/playwright/tests/fixtures/theia-fixture.ts b/examples/playwright/tests/fixtures/theia-fixture.ts
new file mode 100644
index 0000000000000..80ce5101c5788
--- /dev/null
+++ b/examples/playwright/tests/fixtures/theia-fixture.ts
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import test, { Page } from '@playwright/test';
+
+export let page: Page;
+
+test.beforeAll(async ({ browser }) => {
+ page = await browser.newPage();
+});
+
+export default test;
diff --git a/examples/playwright/tests/resources/sample-files1/sample.txt b/examples/playwright/tests/resources/sample-files1/sample.txt
new file mode 100644
index 0000000000000..09cbd5af540d9
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sample.txt
@@ -0,0 +1,4 @@
+this is just a sample file
+content line 2
+content line 3
+content line 4
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-2.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-2.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-2.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-1.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-1.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-1.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-2.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-2.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-2.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-1.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-1.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-1.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-2.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-2.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-2.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-1.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-1.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-1.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-2.txt b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-2.txt
new file mode 100644
index 0000000000000..b72d61c7caf1a
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-2.txt
@@ -0,0 +1 @@
+this is just a sample file
diff --git a/examples/playwright/tests/resources/sample-files2/another-sample.txt b/examples/playwright/tests/resources/sample-files2/another-sample.txt
new file mode 100644
index 0000000000000..7749264bcc30f
--- /dev/null
+++ b/examples/playwright/tests/resources/sample-files2/another-sample.txt
@@ -0,0 +1 @@
+this is just another sample file
diff --git a/examples/playwright/tests/theia-app.test.ts b/examples/playwright/tests/theia-app.test.ts
new file mode 100644
index 0000000000000..0660d8fe4919b
--- /dev/null
+++ b/examples/playwright/tests/theia-app.test.ts
@@ -0,0 +1,34 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaApp } from '../src/theia-app';
+
+import { expect } from '@playwright/test';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+
+test.describe('Theia Application', () => {
+
+ test('should load', async () => {
+ app = await TheiaApp.load(page);
+ });
+
+ test('should show main content panel', async () => {
+ expect(await app.isMainContentPanelVisible()).toBe(true);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-explorer-view.test.ts b/examples/playwright/tests/theia-explorer-view.test.ts
new file mode 100644
index 0000000000000..9a2f969410e06
--- /dev/null
+++ b/examples/playwright/tests/theia-explorer-view.test.ts
@@ -0,0 +1,124 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { DOT_FILES_FILTER, TheiaExplorerView } from '../src/theia-explorer-view';
+import { TheiaWorkspace } from '../src/theia-workspace';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+let explorer: TheiaExplorerView;
+
+test.describe('Theia Explorer View', () => {
+
+ test.beforeAll(async () => {
+ const ws = new TheiaWorkspace(['tests/resources/sample-files1']);
+ app = await TheiaApp.load(page, ws);
+ explorer = await app.openView(TheiaExplorerView);
+ });
+
+ test('should be visible and active after being opened', async () => {
+ expect(await explorer.isTabVisible()).toBe(true);
+ expect(await explorer.isDisplayed()).toBe(true);
+ expect(await explorer.isActive()).toBe(true);
+ });
+
+ test("should be opened at the left and have the title 'Explorer'", async () => {
+ expect(await explorer.isInSidePanel()).toBe(true);
+ expect(await explorer.side()).toBe('left');
+ expect(await explorer.title()).toBe('Explorer');
+ });
+
+ test('should be possible to close and reopen it', async () => {
+ await explorer.close();
+ expect(await explorer.isTabVisible()).toBe(false);
+
+ explorer = await app.openView(TheiaExplorerView);
+ expect(await explorer.isTabVisible()).toBe(true);
+ expect(await explorer.isDisplayed()).toBe(true);
+ expect(await explorer.isActive()).toBe(true);
+ });
+
+ test('should show one folder named "sampleFolder" and one file named "sample.txt"', async () => {
+ await explorer.selectTreeNode('sampleFolder');
+ expect(await explorer.isTreeNodeSelected('sampleFolder')).toBe(true);
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
+ expect(fileStatElements.length).toBe(2);
+
+ let file; let folder;
+ if (await fileStatElements[0].isFolder()) {
+ folder = fileStatElements[0];
+ file = fileStatElements[1];
+ } else {
+ folder = fileStatElements[1];
+ file = fileStatElements[0];
+ }
+
+ expect(await folder.label()).toBe('sampleFolder');
+ expect(await folder.isFile()).toBe(false);
+ expect(await folder.isFolder()).toBe(true);
+ expect(await file.label()).toBe('sample.txt');
+ expect(await file.isFolder()).toBe(false);
+ expect(await file.isFile()).toBe(true);
+ });
+
+ test('should provide file stat node by single path fragment "sample.txt"', async () => {
+ const file = await explorer.getFileStatNodeByLabel('sample.txt');
+ expect(await file.label()).toBe('sample.txt');
+ expect(await file.isFolder()).toBe(false);
+ expect(await file.isFile()).toBe(true);
+ });
+
+ test('should provide file stat nodes that can define whether they are collapsed or not and that can be expanded', async () => {
+ const file = await explorer.getFileStatNodeByLabel('sample.txt');
+ expect(await file.isCollapsed()).toBe(false);
+
+ const folder = await explorer.getFileStatNodeByLabel('sampleFolder');
+ expect(await folder.isCollapsed()).toBe(true);
+
+ await folder.expand();
+ expect(await folder.isCollapsed()).toBe(false);
+ });
+
+ test('should provide file stat node by path "sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt"', async () => {
+ const file = await explorer.fileStatNode('sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt');
+ if (!file) { throw Error('File stat node could not be retrieved by path'); }
+ expect(await file.label()).toBe('sampleFile1-1-1.txt');
+ });
+
+ test('should open context menu on "sample.txt"', async () => {
+ const file = await explorer.getFileStatNodeByLabel('sample.txt');
+ const menu = await file.openContextMenu();
+ expect(await menu.isOpen()).toBe(true);
+
+ const menuItems = await menu.visibleMenuItems();
+ expect(menuItems).toContain('Open');
+ expect(menuItems).toContain('Delete');
+ expect(menuItems).toContain('Download');
+
+ await menu.close();
+ expect(await menu.isOpen()).toBe(false);
+ });
+
+ test('should rename "sample.txt"', async () => {
+ await explorer.renameNode('sample.txt', 'sample-new.txt');
+ expect(await explorer.existsFileNode('sample-new.txt')).toBe(true);
+ await explorer.renameNode('sample-new.txt', 'sample.txt');
+ expect(await explorer.existsFileNode('sample.txt')).toBe(true);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-main-menu.test.ts b/examples/playwright/tests/theia-main-menu.test.ts
new file mode 100644
index 0000000000000..b4e3a45c058c2
--- /dev/null
+++ b/examples/playwright/tests/theia-main-menu.test.ts
@@ -0,0 +1,88 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { TheiaMenuBar } from '../src/theia-main-menu';
+import test, { page } from './fixtures/theia-fixture';
+
+let menuBar: TheiaMenuBar;
+
+test.describe('Theia Main Menu', () => {
+
+ test.beforeAll(async () => {
+ const app = await TheiaApp.load(page);
+ menuBar = app.menuBar;
+ });
+
+ test('should show the main menu bar', async () => {
+ const menuBarItems = await menuBar.visibleMenuBarItems();
+ expect(menuBarItems).toContain('File');
+ expect(menuBarItems).toContain('Edit');
+ expect(menuBarItems).toContain('Help');
+ });
+
+ test("should open main menu 'File'", async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ expect(await mainMenu.isOpen()).toBe(true);
+ });
+
+ test("should show the menu items 'New File' and 'New Folder'", async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ const menuItems = await mainMenu.visibleMenuItems();
+ expect(menuItems).toContain('New File');
+ expect(menuItems).toContain('New Folder');
+ });
+
+ test("should return menu item by name 'New File'", async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ const menuItem = await mainMenu.menuItemByName('New File');
+ expect(menuItem).toBeDefined();
+
+ const label = await menuItem?.label();
+ expect(label).toBe('New File');
+
+ const shortCut = await menuItem?.shortCut();
+ expect(shortCut).toBe('Alt+N');
+
+ const hasSubmenu = await menuItem?.hasSubmenu();
+ expect(hasSubmenu).toBe(false);
+ });
+
+ test('should detect whether menu item has submenu', async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ const newFileItem = await mainMenu.menuItemByName('New File');
+ const settingsItem = await mainMenu.menuItemByName('Preferences');
+
+ expect(await newFileItem?.hasSubmenu()).toBe(false);
+ expect(await settingsItem?.hasSubmenu()).toBe(true);
+ });
+
+ test('should be able to show menu item in submenu by path', async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ const openPreferencesItem = await mainMenu.menuItemByNamePath('Preferences', 'Open Settings (UI)');
+
+ const label = await openPreferencesItem?.label();
+ expect(label).toBe('Open Settings (UI)');
+ });
+
+ test('should close main menu', async () => {
+ const mainMenu = await menuBar.openMenu('File');
+ await mainMenu.close();
+ expect(await mainMenu.isOpen()).toBe(false);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-preference-view.test.ts b/examples/playwright/tests/theia-preference-view.test.ts
new file mode 100644
index 0000000000000..6d3ec5a10e92e
--- /dev/null
+++ b/examples/playwright/tests/theia-preference-view.test.ts
@@ -0,0 +1,93 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../src/theia-preference-view';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+
+test.describe('Preference View', () => {
+
+ test.beforeAll(async () => {
+ app = await TheiaApp.load(page);
+ });
+
+ test('should be visible and active after being opened', async () => {
+ const preferenceView = await app.openPreferences(TheiaPreferenceView);
+ expect(await preferenceView.isTabVisible()).toBe(true);
+ expect(await preferenceView.isDisplayed()).toBe(true);
+ expect(await preferenceView.isActive()).toBe(true);
+ });
+
+ test('should be able to read, set, and reset String preferences', async () => {
+ const preferences = await app.openPreferences(TheiaPreferenceView);
+ const preferenceId = PreferenceIds.DiffEditor.MaxComputationTime;
+
+ await preferences.resetStringPreferenceById(preferenceId);
+ expect(await preferences.getStringPreferenceById(preferenceId)).toBe(DefaultPreferences.DiffEditor.MaxComputationTime);
+
+ await preferences.setStringPreferenceById(preferenceId, '8000');
+ await preferences.waitForModified(preferenceId);
+ expect(await preferences.getStringPreferenceById(preferenceId)).toBe('8000');
+
+ await preferences.resetStringPreferenceById(preferenceId);
+ expect(await preferences.getStringPreferenceById(preferenceId)).toBe(DefaultPreferences.DiffEditor.MaxComputationTime);
+ });
+
+ test('should be able to read, set, and reset Boolean preferences', async () => {
+ const preferences = await app.openPreferences(TheiaPreferenceView);
+ const preferenceId = PreferenceIds.Explorer.AutoReveal;
+
+ await preferences.resetBooleanPreferenceById(preferenceId);
+ expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(DefaultPreferences.Explorer.AutoReveal.Enabled);
+
+ await preferences.setBooleanPreferenceById(preferenceId, false);
+ await preferences.waitForModified(preferenceId);
+ expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(false);
+
+ await preferences.resetBooleanPreferenceById(preferenceId);
+ expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(DefaultPreferences.Explorer.AutoReveal.Enabled);
+ });
+
+ test('should throw an error if we try to read, set, or reset a non-existing preference', async () => {
+ const preferences = await app.openPreferences(TheiaPreferenceView);
+
+ preferences.customTimeout = 500;
+ try {
+ await expect(preferences.getBooleanPreferenceById('no.a.real.preference')).rejects.toThrowError();
+ await expect(preferences.setBooleanPreferenceById('no.a.real.preference', true)).rejects.toThrowError();
+ await expect(preferences.resetBooleanPreferenceById('no.a.real.preference')).rejects.toThrowError();
+
+ await expect(preferences.getStringPreferenceById('no.a.real.preference')).rejects.toThrowError();
+ await expect(preferences.setStringPreferenceById('no.a.real.preference', 'a')).rejects.toThrowError();
+ await expect(preferences.resetStringPreferenceById('no.a.real.preference')).rejects.toThrowError();
+ } finally {
+ preferences.customTimeout = undefined;
+ }
+ });
+
+ test('should throw an error if we try to read, or set a preference with the wrong type', async () => {
+ const preferences = await app.openPreferences(TheiaPreferenceView);
+ const stringPreference = PreferenceIds.DiffEditor.MaxComputationTime;
+ const booleanPreference = PreferenceIds.Explorer.AutoReveal;
+
+ await expect(preferences.getBooleanPreferenceById(stringPreference)).rejects.toThrowError();
+ await expect(preferences.setBooleanPreferenceById(stringPreference, true)).rejects.toThrowError();
+ await expect(preferences.setStringPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
+ });
+});
diff --git a/examples/playwright/tests/theia-problems-view.test.ts b/examples/playwright/tests/theia-problems-view.test.ts
new file mode 100644
index 0000000000000..e5f90bf85a00d
--- /dev/null
+++ b/examples/playwright/tests/theia-problems-view.test.ts
@@ -0,0 +1,60 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { TheiaProblemsView } from '../src/theia-problem-view';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+
+test.describe('Theia Problems View', () => {
+
+ test.beforeAll(async () => {
+ app = await TheiaApp.load(page);
+ });
+
+ test('should be visible and active after being opened', async () => {
+ const problemsView = await app.openView(TheiaProblemsView);
+ expect(await problemsView.isTabVisible()).toBe(true);
+ expect(await problemsView.isDisplayed()).toBe(true);
+ expect(await problemsView.isActive()).toBe(true);
+ });
+
+ test("should be opened at the bottom and have the title 'Problems'", async () => {
+ const problemsView = await app.openView(TheiaProblemsView);
+ expect(await problemsView.isInSidePanel()).toBe(false);
+ expect(await problemsView.side()).toBe('bottom');
+ expect(await problemsView.title()).toBe('Problems');
+ });
+
+ test('should be closable', async () => {
+ const problemsView = await app.openView(TheiaProblemsView);
+ expect(await problemsView.isClosable()).toBe(true);
+
+ await problemsView.close();
+ expect(await problemsView.isTabVisible()).toBe(false);
+ expect(await problemsView.isDisplayed()).toBe(false);
+ expect(await problemsView.isActive()).toBe(false);
+ });
+
+ test("should not throw an error if 'close' is called twice", async () => {
+ const problemsView = await app.openView(TheiaProblemsView);
+ await problemsView.close();
+ await problemsView.close();
+ });
+
+});
diff --git a/examples/playwright/tests/theia-quick-command.test.ts b/examples/playwright/tests/theia-quick-command.test.ts
new file mode 100644
index 0000000000000..c6afbd6bd02b4
--- /dev/null
+++ b/examples/playwright/tests/theia-quick-command.test.ts
@@ -0,0 +1,61 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaAboutDialog } from '../src/theia-about-dialog';
+import { TheiaApp } from '../src/theia-app';
+import { TheiaExplorerView } from '../src/theia-explorer-view';
+import { TheiaQuickCommandPalette } from '../src/theia-quick-command-palette';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+let quickCommand: TheiaQuickCommandPalette;
+
+test.describe('Theia Quick Command', () => {
+
+ test.beforeAll(async () => {
+ app = await TheiaApp.load(page);
+ quickCommand = app.quickCommandPalette;
+ });
+
+ test('should show quick command palette', async () => {
+ await quickCommand.open();
+ expect(await quickCommand.isOpen()).toBe(true);
+ });
+
+ test('should trigger \'About\' command after typing', async () => {
+ await quickCommand.type('About');
+ await quickCommand.trigger('About');
+ expect(await quickCommand.isOpen()).toBe(false);
+ const aboutDialog = new TheiaAboutDialog(app);
+ expect(await aboutDialog.isVisible()).toBe(true);
+ await aboutDialog.close();
+ expect(await aboutDialog.isVisible()).toBe(false);
+
+ await quickCommand.type('Select All');
+ await quickCommand.trigger('Select All');
+ expect(await quickCommand.isOpen()).toBe(false);
+ });
+
+ test('should trigger \'Toggle Explorer View\' command after typing', async () => {
+ await quickCommand.type('Toggle Explorer');
+ await quickCommand.trigger('Toggle Explorer View');
+ expect(await quickCommand.isOpen()).toBe(false);
+ const explorerView = new TheiaExplorerView(app);
+ expect(await explorerView.isDisplayed()).toBe(true);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-status-bar.test.ts b/examples/playwright/tests/theia-status-bar.test.ts
new file mode 100644
index 0000000000000..588feb1fd2863
--- /dev/null
+++ b/examples/playwright/tests/theia-status-bar.test.ts
@@ -0,0 +1,47 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { TheiaNotificationIndicator } from '../src/theia-notification-indicator';
+import { TheiaProblemIndicator } from '../src/theia-problem-indicator';
+import { TheiaStatusBar } from '../src/theia-status-bar';
+import { TheiaToggleBottomIndicator } from '../src/theia-toggle-bottom-indicator';
+import test, { page } from './fixtures/theia-fixture';
+
+let statusBar: TheiaStatusBar;
+
+test.describe('Theia Status Bar', () => {
+
+ test.beforeAll(async () => {
+ const app = await TheiaApp.load(page);
+ statusBar = app.statusBar;
+ });
+
+ test('should show status bar', async () => {
+ expect(await statusBar.isVisible()).toBe(true);
+ });
+
+ test('should contain status bar elements', async () => {
+ const problemIndicator = await statusBar.statusIndicator(TheiaProblemIndicator);
+ const notificationIndicator = await statusBar.statusIndicator(TheiaNotificationIndicator);
+ const toggleBottomIndicator = await statusBar.statusIndicator(TheiaToggleBottomIndicator);
+ expect(await problemIndicator.isVisible()).toBe(true);
+ expect(await notificationIndicator.isVisible()).toBe(true);
+ expect(await toggleBottomIndicator.isVisible()).toBe(true);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-text-editor.test.ts b/examples/playwright/tests/theia-text-editor.test.ts
new file mode 100644
index 0000000000000..24f39523eabb9
--- /dev/null
+++ b/examples/playwright/tests/theia-text-editor.test.ts
@@ -0,0 +1,176 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { expect } from '@playwright/test';
+import { TheiaApp } from '../src/theia-app';
+import { TheiaTextEditor } from '../src/theia-text-editor';
+import { TheiaWorkspace } from '../src/theia-workspace';
+import test, { page } from './fixtures/theia-fixture';
+
+let app: TheiaApp;
+
+test.describe('Theia Text Editor', () => {
+
+ test.beforeAll(async () => {
+ const ws = new TheiaWorkspace(['tests/resources/sample-files1']);
+ app = await TheiaApp.load(page, ws);
+ });
+
+ test('should be visible and active after opening "sample.txt"', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ expect(await sampleTextEditor.isTabVisible()).toBe(true);
+ expect(await sampleTextEditor.isDisplayed()).toBe(true);
+ expect(await sampleTextEditor.isActive()).toBe(true);
+ });
+
+ test('should be possible to open "sample.txt" when already opened and then close it', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ expect(await sampleTextEditor.isTabVisible()).toBe(true);
+
+ await sampleTextEditor.close();
+ expect(await sampleTextEditor.isTabVisible()).toBe(false);
+ });
+
+ test('should be possible to open four text editors, switch among them, and close them', async () => {
+ const textEditor1_1_1 = await app.openEditor('sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt', TheiaTextEditor);
+ const textEditor1_1_2 = await app.openEditor('sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-2.txt', TheiaTextEditor);
+ const textEditor1_2_1 = await app.openEditor('sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-1.txt', TheiaTextEditor);
+ const textEditor1_2_2 = await app.openEditor('sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-2.txt', TheiaTextEditor);
+ const allEditors = [textEditor1_1_1, textEditor1_1_2, textEditor1_2_1, textEditor1_2_2];
+
+ // all editor tabs should be visible
+ for (const editor of allEditors) {
+ expect(await editor.isTabVisible()).toBe(true);
+ }
+
+ // activate one editor after the other and check that only this editor is active
+ for (const editor of allEditors) {
+ await editor.activate();
+ expect(await textEditor1_1_1.isActive()).toBe(textEditor1_1_1 === editor);
+ expect(await textEditor1_1_2.isActive()).toBe(textEditor1_1_2 === editor);
+ expect(await textEditor1_2_1.isActive()).toBe(textEditor1_2_1 === editor);
+ expect(await textEditor1_2_2.isActive()).toBe(textEditor1_2_2 === editor);
+ }
+
+ // close all editors
+ for (const editor of allEditors) {
+ await editor.close();
+ }
+
+ // check that all editors are closed
+ for (const editor of allEditors) {
+ expect(await editor.isTabVisible()).toBe(false);
+ }
+ });
+
+ test('should return the contents of lines by line number', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe('content line 2');
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(3)).toBe('content line 3');
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(4)).toBe('content line 4');
+ await sampleTextEditor.close();
+ });
+
+ test('should return the contents of lines containing text', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ expect(await sampleTextEditor.textContentOfLineContainingText('line 2')).toBe('content line 2');
+ expect(await sampleTextEditor.textContentOfLineContainingText('line 3')).toBe('content line 3');
+ expect(await sampleTextEditor.textContentOfLineContainingText('line 4')).toBe('content line 4');
+ await sampleTextEditor.close();
+ });
+
+ test('should be dirty after changing the file contents and clean after save', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.replaceLineWithLineNumber('this is just a sample file', 1);
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ await sampleTextEditor.save();
+ expect(await sampleTextEditor.isDirty()).toBe(false);
+ await sampleTextEditor.close();
+ });
+
+ test('should replace the line with line number 2 with new text "new -- content line 2 -- new"', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.replaceLineWithLineNumber('new -- content line 2 -- new', 2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe('new -- content line 2 -- new');
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ await sampleTextEditor.save();
+ expect(await sampleTextEditor.isDirty()).toBe(false);
+ await sampleTextEditor.close();
+ });
+
+ test('should replace the line with containing text "content line 2" with "even newer -- content line 2 -- even newer"', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.replaceLineContainingText('even newer -- content line 2 -- even newer', 'content line 2');
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe('even newer -- content line 2 -- even newer');
+ await sampleTextEditor.saveAndClose();
+ });
+
+ test('should delete the line with containing text "content line 2"', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.deleteLineContainingText('content line 2');
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe('content line 3');
+ await sampleTextEditor.saveAndClose();
+ });
+
+ test('should delete the line with line number 2', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.deleteLineByLineNumber(2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe('content line 4');
+ await sampleTextEditor.saveAndClose();
+ });
+
+ test('should have more lines after adding text in new line after line containing text "sample file"', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ const numberOfLinesBefore = await sampleTextEditor.numberOfLines();
+
+ await sampleTextEditor.addTextToNewLineAfterLineContainingText('sample file', 'new content for line 2');
+ const numberOfLinesAfter = await sampleTextEditor.numberOfLines();
+ expect(numberOfLinesBefore).not.toBeUndefined();
+ expect(numberOfLinesAfter).not.toBeUndefined();
+ expect(numberOfLinesAfter).toBeGreaterThan(numberOfLinesBefore!);
+
+ await sampleTextEditor.saveAndClose();
+ });
+
+ test('should undo and redo text changes with correctly updated dirty states', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.replaceLineWithLineNumber('change', 1);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe('change');
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ await sampleTextEditor.undo(2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe('this is just a sample file');
+ expect(await sampleTextEditor.isDirty()).toBe(false);
+
+ await sampleTextEditor.redo(2);
+ expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe('change');
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ await sampleTextEditor.saveAndClose();
+ });
+
+ test('should close without saving', async () => {
+ const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
+ await sampleTextEditor.replaceLineWithLineNumber('change again', 1);
+ expect(await sampleTextEditor.isDirty()).toBe(true);
+
+ await sampleTextEditor.closeWithoutSave();
+ expect(await sampleTextEditor.isTabVisible()).toBe(false);
+ });
+
+});
diff --git a/examples/playwright/tests/theia-workspace.test.ts b/examples/playwright/tests/theia-workspace.test.ts
new file mode 100644
index 0000000000000..23e6577dd3fa6
--- /dev/null
+++ b/examples/playwright/tests/theia-workspace.test.ts
@@ -0,0 +1,51 @@
+/********************************************************************************
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource 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
+ ********************************************************************************/
+
+import { TheiaApp } from '../src/theia-app';
+import { DOT_FILES_FILTER, TheiaExplorerView } from '../src/theia-explorer-view';
+import { TheiaWorkspace } from '../src/theia-workspace';
+import { expect } from '@playwright/test';
+import test, { page } from './fixtures/theia-fixture';
+
+test.describe('Theia Workspace', () => {
+
+ test('should be initialized empty by default', async () => {
+ const app = await TheiaApp.load(page);
+ const explorer = await app.openView(TheiaExplorerView);
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
+ expect(fileStatElements.length).toBe(0);
+ });
+
+ test('should be initialized with the contents of a file location', async () => {
+ const ws = new TheiaWorkspace(['tests/resources/sample-files1']);
+ const app = await TheiaApp.load(page, ws);
+ const explorer = await app.openView(TheiaExplorerView);
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
+ // resources/sample-files1 contains one folder and one file
+ expect(fileStatElements.length).toBe(2);
+ });
+
+ test('should be initialized with the contents of multiple file locations', async () => {
+ const ws = new TheiaWorkspace(['tests/resources/sample-files1', 'tests/resources/sample-files2']);
+ const app = await TheiaApp.load(page, ws);
+ const explorer = await app.openView(TheiaExplorerView);
+ const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
+ // resources/sample-files1 contains one folder and one file
+ // resources/sample-files2 contains one file
+ expect(fileStatElements.length).toBe(3);
+ });
+
+});
diff --git a/examples/playwright/tsconfig.json b/examples/playwright/tsconfig.json
new file mode 100644
index 0000000000000..ece88b92c742b
--- /dev/null
+++ b/examples/playwright/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../configs/base.tsconfig",
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2018",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "outDir": "lib",
+ "baseUrl": ".",
+ "rootDir": "."
+ },
+ "include": [
+ "src",
+ "tests",
+ "configs"
+ ],
+ "references": []
+}
diff --git a/tsconfig.json b/tsconfig.json
index d980c388814b0..8bfb360daa093 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -33,6 +33,9 @@
{
"path": "examples/electron"
},
+ {
+ "path": "examples/playwright"
+ },
{
"path": "packages/bulk-edit"
},
diff --git a/yarn.lock b/yarn.lock
index e842289ba1266..9f4ae8b57c2d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16,6 +16,13 @@
dependencies:
"@babel/highlight" "^7.16.0"
+"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
+ integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
+ dependencies:
+ "@babel/highlight" "^7.16.7"
+
"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.0", "@babel/compat-data@^7.16.4":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e"
@@ -42,6 +49,27 @@
semver "^6.3.0"
source-map "^0.5.0"
+"@babel/core@^7.14.8":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.7.tgz#db990f931f6d40cb9b87a0dc7d2adc749f1dcbcf"
+ integrity sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==
+ dependencies:
+ "@babel/code-frame" "^7.16.7"
+ "@babel/generator" "^7.16.7"
+ "@babel/helper-compilation-targets" "^7.16.7"
+ "@babel/helper-module-transforms" "^7.16.7"
+ "@babel/helpers" "^7.16.7"
+ "@babel/parser" "^7.16.7"
+ "@babel/template" "^7.16.7"
+ "@babel/traverse" "^7.16.7"
+ "@babel/types" "^7.16.7"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.1.2"
+ semver "^6.3.0"
+ source-map "^0.5.0"
+
"@babel/generator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2"
@@ -51,6 +79,15 @@
jsesc "^2.5.1"
source-map "^0.5.0"
+"@babel/generator@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.7.tgz#b42bf46a3079fa65e1544135f32e7958f048adbb"
+ integrity sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==
+ dependencies:
+ "@babel/types" "^7.16.7"
+ jsesc "^2.5.1"
+ source-map "^0.5.0"
+
"@babel/helper-annotate-as-pure@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d"
@@ -58,6 +95,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-annotate-as-pure@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862"
+ integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.0.tgz#f1a686b92da794020c26582eb852e9accd0d7882"
@@ -76,6 +120,16 @@
browserslist "^4.17.5"
semver "^6.3.0"
+"@babel/helper-compilation-targets@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b"
+ integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==
+ dependencies:
+ "@babel/compat-data" "^7.16.4"
+ "@babel/helper-validator-option" "^7.16.7"
+ browserslist "^4.17.5"
+ semver "^6.3.0"
+
"@babel/helper-create-class-features-plugin@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz#090d4d166b342a03a9fec37ef4fd5aeb9c7c6a4b"
@@ -88,6 +142,19 @@
"@babel/helper-replace-supers" "^7.16.0"
"@babel/helper-split-export-declaration" "^7.16.0"
+"@babel/helper-create-class-features-plugin@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.7.tgz#9c5b34b53a01f2097daf10678d65135c1b9f84ba"
+ integrity sha512-kIFozAvVfK05DM4EVQYKK+zteWvY85BFdGBRQBytRyY3y+6PX0DkDOn/CZ3lEuczCfrCxEzwt0YtP/87YPTWSw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.16.7"
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-function-name" "^7.16.7"
+ "@babel/helper-member-expression-to-functions" "^7.16.7"
+ "@babel/helper-optimise-call-expression" "^7.16.7"
+ "@babel/helper-replace-supers" "^7.16.7"
+ "@babel/helper-split-export-declaration" "^7.16.7"
+
"@babel/helper-create-regexp-features-plugin@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz#06b2348ce37fccc4f5e18dcd8d75053f2a7c44ff"
@@ -110,6 +177,13 @@
resolve "^1.14.2"
semver "^6.1.2"
+"@babel/helper-environment-visitor@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7"
+ integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-explode-assignable-expression@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778"
@@ -126,6 +200,15 @@
"@babel/template" "^7.16.0"
"@babel/types" "^7.16.0"
+"@babel/helper-function-name@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f"
+ integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.16.7"
+ "@babel/template" "^7.16.7"
+ "@babel/types" "^7.16.7"
+
"@babel/helper-get-function-arity@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa"
@@ -133,6 +216,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-get-function-arity@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419"
+ integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-hoist-variables@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a"
@@ -140,6 +230,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-hoist-variables@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246"
+ integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-member-expression-to-functions@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4"
@@ -147,6 +244,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-member-expression-to-functions@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0"
+ integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
@@ -154,6 +258,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-module-imports@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
+ integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-module-transforms@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5"
@@ -168,6 +279,20 @@
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
+"@babel/helper-module-transforms@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41"
+ integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-module-imports" "^7.16.7"
+ "@babel/helper-simple-access" "^7.16.7"
+ "@babel/helper-split-export-declaration" "^7.16.7"
+ "@babel/helper-validator-identifier" "^7.16.7"
+ "@babel/template" "^7.16.7"
+ "@babel/traverse" "^7.16.7"
+ "@babel/types" "^7.16.7"
+
"@babel/helper-optimise-call-expression@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338"
@@ -175,11 +300,23 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-optimise-call-expression@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2"
+ integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
+"@babel/helper-plugin-utils@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
+ integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
+
"@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.4":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e"
@@ -199,6 +336,17 @@
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
+"@babel/helper-replace-supers@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1"
+ integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-member-expression-to-functions" "^7.16.7"
+ "@babel/helper-optimise-call-expression" "^7.16.7"
+ "@babel/traverse" "^7.16.7"
+ "@babel/types" "^7.16.7"
+
"@babel/helper-simple-access@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517"
@@ -206,6 +354,13 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-simple-access@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7"
+ integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-skip-transparent-expression-wrappers@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09"
@@ -220,16 +375,33 @@
dependencies:
"@babel/types" "^7.16.0"
+"@babel/helper-split-export-declaration@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b"
+ integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==
+ dependencies:
+ "@babel/types" "^7.16.7"
+
"@babel/helper-validator-identifier@^7.15.7":
version "7.15.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
+"@babel/helper-validator-identifier@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
+ integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
+
"@babel/helper-validator-option@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
+"@babel/helper-validator-option@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23"
+ integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==
+
"@babel/helper-wrap-function@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c"
@@ -249,6 +421,15 @@
"@babel/traverse" "^7.16.3"
"@babel/types" "^7.16.0"
+"@babel/helpers@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc"
+ integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==
+ dependencies:
+ "@babel/template" "^7.16.7"
+ "@babel/traverse" "^7.16.7"
+ "@babel/types" "^7.16.7"
+
"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a"
@@ -258,11 +439,25 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/highlight@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.7.tgz#81a01d7d675046f0d96f82450d9d9578bdfd6b0b"
+ integrity sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.16.7"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
"@babel/parser@^7.16.0", "@babel/parser@^7.16.3":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e"
integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==
+"@babel/parser@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.7.tgz#d372dda9c89fcec340a82630a9f533f2fe15877e"
+ integrity sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2":
version "7.16.2"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz#2977fca9b212db153c195674e57cfab807733183"
@@ -288,6 +483,14 @@
"@babel/helper-remap-async-to-generator" "^7.16.4"
"@babel/plugin-syntax-async-generators" "^7.8.4"
+"@babel/plugin-proposal-class-properties@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0"
+ integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.16.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-proposal-class-properties@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz#c029618267ddebc7280fa286e0f8ca2a278a2d1a"
@@ -305,6 +508,14 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
+"@babel/plugin-proposal-dynamic-import@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2"
+ integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+
"@babel/plugin-proposal-dynamic-import@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.0.tgz#783eca61d50526202f9b296095453977e88659f1"
@@ -313,6 +524,14 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
+"@babel/plugin-proposal-export-namespace-from@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163"
+ integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+
"@babel/plugin-proposal-export-namespace-from@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.0.tgz#9c01dee40b9d6b847b656aaf4a3976a71740f222"
@@ -329,6 +548,14 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-json-strings" "^7.8.3"
+"@babel/plugin-proposal-logical-assignment-operators@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea"
+ integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+
"@babel/plugin-proposal-logical-assignment-operators@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.0.tgz#a711b8ceb3ffddd3ef88d3a49e86dbd3cc7db3fd"
@@ -337,6 +564,14 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99"
+ integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+
"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.0.tgz#44e1cce08fe2427482cf446a91bb451528ed0596"
@@ -345,6 +580,14 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+"@babel/plugin-proposal-numeric-separator@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9"
+ integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+
"@babel/plugin-proposal-numeric-separator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.0.tgz#5d418e4fbbf8b9b7d03125d3a52730433a373734"
@@ -372,6 +615,15 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+"@babel/plugin-proposal-optional-chaining@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a"
+ integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+
"@babel/plugin-proposal-optional-chaining@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.0.tgz#56dbc3970825683608e9efb55ea82c2a2d6c8dc0"
@@ -381,6 +633,14 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.16.0"
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
+"@babel/plugin-proposal-private-methods@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.7.tgz#e418e3aa6f86edd6d327ce84eff188e479f571e0"
+ integrity sha512-7twV3pzhrRxSwHeIvFE6coPgvo+exNDOiGUMg39o2LiLo1Y+4aKpfkcLGcg1UHonzorCt7SNXnoMyCnnIOA8Sw==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.16.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-proposal-private-methods@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.0.tgz#b4dafb9c717e4301c5776b30d080d6383c89aff6"
@@ -389,6 +649,16 @@
"@babel/helper-create-class-features-plugin" "^7.16.0"
"@babel/helper-plugin-utils" "^7.14.5"
+"@babel/plugin-proposal-private-property-in-object@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce"
+ integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.16.7"
+ "@babel/helper-create-class-features-plugin" "^7.16.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+
"@babel/plugin-proposal-private-property-in-object@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.0.tgz#69e935b2c5c79d2488112d886f0c4e2790fee76f"
@@ -505,6 +775,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
+"@babel/plugin-syntax-typescript@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8"
+ integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-transform-arrow-functions@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.0.tgz#951706f8b449c834ed07bd474c0924c944b95a8e"
@@ -623,6 +900,16 @@
"@babel/helper-plugin-utils" "^7.14.5"
babel-plugin-dynamic-import-node "^2.3.3"
+"@babel/plugin-transform-modules-commonjs@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.7.tgz#fd119e6a433c527d368425b45df361e1e95d3c1a"
+ integrity sha512-h2RP2kE7He1ZWKyAlanMZrAbdv+Acw1pA8dQZhE025WJZE2z0xzFADAinXA9fxd5bn7JnM+SdOGcndGx1ARs9w==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.16.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/helper-simple-access" "^7.16.7"
+ babel-plugin-dynamic-import-node "^2.3.3"
+
"@babel/plugin-transform-modules-commonjs@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.0.tgz#add58e638c8ddc4875bd9a9ecb5c594613f6c922"
@@ -750,6 +1037,15 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
+"@babel/plugin-transform-typescript@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.7.tgz#33f8c2c890fbfdc4ef82446e9abb8de8211a3ff3"
+ integrity sha512-Hzx1lvBtOCWuCEwMmYOfpQpO7joFeXLgoPuzZZBtTxXqSqUGUubvFGZv2ygo1tB5Bp9q6PXV3H0E/kf7KM0RLA==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.16.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-typescript" "^7.16.7"
+
"@babel/plugin-transform-unicode-escapes@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.0.tgz#1a354064b4c45663a32334f46fa0cf6100b5b1f3"
@@ -856,6 +1152,15 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
+"@babel/preset-typescript@^7.14.5":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9"
+ integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/helper-validator-option" "^7.16.7"
+ "@babel/plugin-transform-typescript" "^7.16.7"
+
"@babel/runtime@^7.10.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
@@ -872,6 +1177,15 @@
"@babel/parser" "^7.16.0"
"@babel/types" "^7.16.0"
+"@babel/template@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
+ integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==
+ dependencies:
+ "@babel/code-frame" "^7.16.7"
+ "@babel/parser" "^7.16.7"
+ "@babel/types" "^7.16.7"
+
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787"
@@ -887,6 +1201,22 @@
debug "^4.1.0"
globals "^11.1.0"
+"@babel/traverse@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.7.tgz#dac01236a72c2560073658dd1a285fe4e0865d76"
+ integrity sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==
+ dependencies:
+ "@babel/code-frame" "^7.16.7"
+ "@babel/generator" "^7.16.7"
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-function-name" "^7.16.7"
+ "@babel/helper-hoist-variables" "^7.16.7"
+ "@babel/helper-split-export-declaration" "^7.16.7"
+ "@babel/parser" "^7.16.7"
+ "@babel/types" "^7.16.7"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
"@babel/types@^7.16.0", "@babel/types@^7.4.4":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba"
@@ -895,6 +1225,14 @@
"@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0"
+"@babel/types@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.7.tgz#4ed19d51f840ed4bd5645be6ce40775fecf03159"
+ integrity sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.16.7"
+ to-fast-properties "^2.0.0"
+
"@discoveryjs/json-ext@^0.5.0":
version "0.5.6"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
@@ -971,6 +1309,17 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
+"@jest/types@^27.2.5", "@jest/types@^27.4.2":
+ version "27.4.2"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5"
+ integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^3.0.0"
+ "@types/node" "*"
+ "@types/yargs" "^16.0.0"
+ chalk "^4.0.0"
+
"@lerna/add@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f"
@@ -1933,6 +2282,47 @@
"@phosphor/signaling" "^1.3.1"
"@phosphor/virtualdom" "^1.2.0"
+"@playwright/test@1.17.1":
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.17.1.tgz#9e5aca496d2c90ce95ca19ac2c3a8867a4f606d3"
+ integrity sha512-mMZS5OMTN/vUlqd1JZkFoAk2FsIZ4/E/00tw5it2c/VF4+3z/aWO+PPd8ShEGzYME7B16QGWNPjyFpDQI1t4RQ==
+ dependencies:
+ "@babel/code-frame" "^7.14.5"
+ "@babel/core" "^7.14.8"
+ "@babel/plugin-proposal-class-properties" "^7.14.5"
+ "@babel/plugin-proposal-dynamic-import" "^7.14.5"
+ "@babel/plugin-proposal-export-namespace-from" "^7.14.5"
+ "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5"
+ "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
+ "@babel/plugin-proposal-numeric-separator" "^7.14.5"
+ "@babel/plugin-proposal-optional-chaining" "^7.14.5"
+ "@babel/plugin-proposal-private-methods" "^7.14.5"
+ "@babel/plugin-proposal-private-property-in-object" "^7.14.5"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+ "@babel/plugin-transform-modules-commonjs" "^7.14.5"
+ "@babel/preset-typescript" "^7.14.5"
+ colors "^1.4.0"
+ commander "^8.2.0"
+ debug "^4.1.1"
+ expect "=27.2.5"
+ jest-matcher-utils "=27.2.5"
+ jpeg-js "^0.4.2"
+ mime "^2.4.6"
+ minimatch "^3.0.3"
+ ms "^2.1.2"
+ open "^8.3.0"
+ pirates "^4.0.1"
+ pixelmatch "^5.2.1"
+ playwright-core "=1.17.1"
+ pngjs "^5.0.0"
+ rimraf "^3.0.2"
+ source-map-support "^0.4.18"
+ stack-utils "^2.0.3"
+ yazl "^2.5.1"
+
"@primer/octicons-react@^9.0.0":
version "9.6.0"
resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-9.6.0.tgz#996f621cb063757a4985cd6b45e59ed00e3444bf"
@@ -2146,6 +2536,13 @@
dependencies:
"@types/node" "*"
+"@types/fs-extra@^9.0.8":
+ version "9.0.13"
+ resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
+ integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==
+ dependencies:
+ "@types/node" "*"
+
"@types/glob@*":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -2161,6 +2558,25 @@
dependencies:
highlight.js "*"
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
+ integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
+
+"@types/istanbul-lib-report@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+ integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
+ integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+ dependencies:
+ "@types/istanbul-lib-report" "*"
+
"@types/jsdom@^11.0.4":
version "11.12.0"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-11.12.0.tgz#00ddc6f0a1b04c2f5ff6fb23eb59360ca65f12ae"
@@ -2446,6 +2862,11 @@
dependencies:
"@sinonjs/fake-timers" "^7.1.0"
+"@types/stack-utils@^2.0.0":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
+ integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
+
"@types/tar-fs@^1.16.1":
version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.3.tgz#425b2b817c405d13d051f36ec6ec6ebd25e31069"
@@ -2521,6 +2942,20 @@
dependencies:
"@types/yargs-parser" "*"
+"@types/yargs@^16.0.0":
+ version "16.0.4"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
+ integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
+ dependencies:
+ "@types/yargs-parser" "*"
+
+"@types/yauzl@^2.9.1":
+ version "2.9.2"
+ resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
+ integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
+ dependencies:
+ "@types/node" "*"
+
"@typescript-eslint/eslint-plugin-tslint@^4.8.1":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.33.0.tgz#c0f2a5a8a53a915d6c24983888013b7e78e75b44"
@@ -2931,6 +3366,27 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
+allure-commandline@^2.13.8:
+ version "2.17.2"
+ resolved "https://registry.yarnpkg.com/allure-commandline/-/allure-commandline-2.17.2.tgz#48c1064973619644011092d31294834210c6c433"
+ integrity sha512-2a0M0nX1KtVrg4y0rWEFn/OQkv7AaQSMBOEtlKfQl3heZoTEo0IdB08Uk5vU390+qPsEv3EO5igjyCrS0gX+FQ==
+
+allure-js-commons@2.0.0-beta.14:
+ version "2.0.0-beta.14"
+ resolved "https://registry.yarnpkg.com/allure-js-commons/-/allure-js-commons-2.0.0-beta.14.tgz#a7a830764660a78bafbc7f16a3f392bef76b3a62"
+ integrity sha512-5OlMp8kdc5FuxkquOV1ZXxJCAmFUwHnI5OqL2IlhJ65NKBeABpMwro6BbhlJqcV8iPQmh1w9bErTICSeB+Hj2A==
+ dependencies:
+ mkdirp "^1.0.4"
+ properties "^1.2.1"
+ uuid "^8.3.0"
+
+allure-playwright@^2.0.0-beta.14:
+ version "2.0.0-beta.14"
+ resolved "https://registry.yarnpkg.com/allure-playwright/-/allure-playwright-2.0.0-beta.14.tgz#30639ac569d496e7c5b709ce9ebe932f264e3f9c"
+ integrity sha512-UAhqPOxdFYL/YuoHvrKhdBKviEgHLJubGYdbVNyX04lbbqIx1ddTHE/xo9VmHhOQCiT3WOJDOc7jljuLKom34A==
+ dependencies:
+ allure-js-commons "2.0.0-beta.14"
+
anser@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/anser/-/anser-2.1.0.tgz#a7309c9f29886f19af56cb30c79fc60ea483944e"
@@ -2987,6 +3443,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
+ansi-styles@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+ integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
anymatch@~3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@@ -3851,6 +4312,11 @@ commander@^7.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+commander@^8.2.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
+ integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -4395,6 +4861,11 @@ defer-to-connect@^1.0.1:
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
+define-lazy-prop@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+ integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -4463,6 +4934,11 @@ dicer@0.2.5:
readable-stream "1.1.x"
streamsearch "0.1.2"
+diff-sequences@^27.4.0:
+ version "27.4.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5"
+ integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==
+
diff@3.5.0, diff@^3.4.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -4807,6 +5283,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+escape-string-regexp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+ integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -5097,6 +5578,18 @@ expand-template@^2.0.3:
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+expect@=27.2.5:
+ version "27.2.5"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-27.2.5.tgz#16154aaa60b4d9a5b0adacfea3e4d6178f4b93fd"
+ integrity sha512-ZrO0w7bo8BgGoP/bLz+HDCI+0Hfei9jUSZs5yI/Wyn9VkG9w8oJ7rHRgYj+MA7yqqFa0IwHA3flJzZtYugShJA==
+ dependencies:
+ "@jest/types" "^27.2.5"
+ ansi-styles "^5.0.0"
+ jest-get-type "^27.0.6"
+ jest-matcher-utils "^27.2.5"
+ jest-message-util "^27.2.5"
+ jest-regex-util "^27.0.6"
+
express@^4.16.3:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -5157,6 +5650,17 @@ extract-zip@^1.0.3, extract-zip@^1.6.6:
mkdirp "^0.5.4"
yauzl "^2.10.0"
+extract-zip@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+ integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+ dependencies:
+ debug "^4.1.1"
+ get-stream "^5.1.0"
+ yauzl "^2.10.0"
+ optionalDependencies:
+ "@types/yauzl" "^2.9.1"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -5457,7 +5961,7 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
-fs-extra@^9.0.0, fs-extra@^9.1.0:
+fs-extra@^9.0.0, fs-extra@^9.0.8, fs-extra@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
@@ -6257,6 +6761,11 @@ is-date-object@^1.0.1:
dependencies:
has-tostringtag "^1.0.0"
+is-docker@^2.0.0, is-docker@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
is-electron-renderer@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz#a469d056f975697c58c98c6023eb0aa79af895a2"
@@ -6428,6 +6937,13 @@ is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+is-wsl@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -6514,6 +7030,61 @@ istanbul-reports@^3.0.2:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
+jest-diff@^27.2.5, jest-diff@^27.4.6:
+ version "27.4.6"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d"
+ integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==
+ dependencies:
+ chalk "^4.0.0"
+ diff-sequences "^27.4.0"
+ jest-get-type "^27.4.0"
+ pretty-format "^27.4.6"
+
+jest-get-type@^27.0.6, jest-get-type@^27.4.0:
+ version "27.4.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5"
+ integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==
+
+jest-matcher-utils@=27.2.5:
+ version "27.2.5"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.2.5.tgz#4684faaa8eb32bf15e6edaead6834031897e2980"
+ integrity sha512-qNR/kh6bz0Dyv3m68Ck2g1fLW5KlSOUNcFQh87VXHZwWc/gY6XwnKofx76Qytz3x5LDWT09/2+yXndTkaG4aWg==
+ dependencies:
+ chalk "^4.0.0"
+ jest-diff "^27.2.5"
+ jest-get-type "^27.0.6"
+ pretty-format "^27.2.5"
+
+jest-matcher-utils@^27.2.5:
+ version "27.4.6"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz#53ca7f7b58170638590e946f5363b988775509b8"
+ integrity sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA==
+ dependencies:
+ chalk "^4.0.0"
+ jest-diff "^27.4.6"
+ jest-get-type "^27.4.0"
+ pretty-format "^27.4.6"
+
+jest-message-util@^27.2.5:
+ version "27.4.6"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.4.6.tgz#9fdde41a33820ded3127465e1a5896061524da31"
+ integrity sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA==
+ dependencies:
+ "@babel/code-frame" "^7.12.13"
+ "@jest/types" "^27.4.2"
+ "@types/stack-utils" "^2.0.0"
+ chalk "^4.0.0"
+ graceful-fs "^4.2.4"
+ micromatch "^4.0.4"
+ pretty-format "^27.4.6"
+ slash "^3.0.0"
+ stack-utils "^2.0.3"
+
+jest-regex-util@^27.0.6:
+ version "27.4.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca"
+ integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg==
+
jest-worker@^27.0.6:
version "27.4.4"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.4.tgz#9390a97c013a54d07f5c2ad2b5f6109f30c4966d"
@@ -6523,6 +7094,11 @@ jest-worker@^27.0.6:
merge-stream "^2.0.0"
supports-color "^8.0.0"
+jpeg-js@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
+ integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -7219,7 +7795,7 @@ mime@1.6.0, mime@^1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-mime@^2.0.3, mime@^2.4.4:
+mime@^2.0.3, mime@^2.4.4, mime@^2.4.6:
version "2.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
@@ -7249,7 +7825,7 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4:
+minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@@ -7446,7 +8022,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-ms@^2.0.0, ms@^2.1.1:
+ms@^2.0.0, ms@^2.1.1, ms@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -7996,6 +8572,15 @@ oniguruma@^7.2.0:
dependencies:
nan "^2.14.0"
+open@^8.3.0:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
+ integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
+ dependencies:
+ define-lazy-prop "^2.0.0"
+ is-docker "^2.1.1"
+ is-wsl "^2.2.0"
+
optimist@~0.3.5:
version "0.3.7"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9"
@@ -8403,6 +8988,18 @@ pinkie@^2.0.0:
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+pirates@^4.0.1:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6"
+ integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==
+
+pixelmatch@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65"
+ integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==
+ dependencies:
+ pngjs "^4.0.1"
+
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
@@ -8424,11 +9021,43 @@ pkg-up@^3.0.1:
dependencies:
find-up "^3.0.0"
+playwright-core@=1.17.1:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.17.1.tgz#a16e0f89284a0ed8ae6d77e1c905c84b8a2ba022"
+ integrity sha512-C3c8RpPiC3qr15fRDN6dx6WnUkPLFmST37gms2aoHPDRvp7EaGDPMMZPpqIm/QWB5J40xDrQCD4YYHz2nBTojQ==
+ dependencies:
+ commander "^8.2.0"
+ debug "^4.1.1"
+ extract-zip "^2.0.1"
+ https-proxy-agent "^5.0.0"
+ jpeg-js "^0.4.2"
+ mime "^2.4.6"
+ pngjs "^5.0.0"
+ progress "^2.0.3"
+ proper-lockfile "^4.1.1"
+ proxy-from-env "^1.1.0"
+ rimraf "^3.0.2"
+ socks-proxy-agent "^6.1.0"
+ stack-utils "^2.0.3"
+ ws "^7.4.6"
+ yauzl "^2.10.0"
+ yazl "^2.5.1"
+
pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+pngjs@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe"
+ integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==
+
+pngjs@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
+ integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
+
postcss-modules-extract-imports@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
@@ -8534,6 +9163,15 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+pretty-format@^27.2.5, pretty-format@^27.4.6:
+ version "27.4.6"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7"
+ integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==
+ dependencies:
+ ansi-regex "^5.0.1"
+ ansi-styles "^5.0.0"
+ react-is "^17.0.1"
+
private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -8592,6 +9230,20 @@ prop-types@^15.5.6, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
+proper-lockfile@^4.1.1:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
+ integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==
+ dependencies:
+ graceful-fs "^4.2.4"
+ retry "^0.12.0"
+ signal-exit "^3.0.2"
+
+properties@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/properties/-/properties-1.2.1.tgz#0ee97a7fc020b1a2a55b8659eda4aa8d869094bd"
+ integrity sha1-Dul6f8AgsaKlW4ZZ7aSqjYaQlL0=
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -8797,6 +9449,11 @@ react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-is@^17.0.1:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
@@ -9595,7 +10252,7 @@ socks-proxy-agent@^5.0.0:
debug "4"
socks "^2.3.3"
-socks-proxy-agent@^6.0.0:
+socks-proxy-agent@^6.0.0, socks-proxy-agent@^6.1.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87"
integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==
@@ -9645,6 +10302,13 @@ source-map-loader@^2.0.1:
iconv-lite "^0.6.2"
source-map-js "^0.6.2"
+source-map-support@^0.4.18:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
+ dependencies:
+ source-map "^0.5.6"
+
source-map-support@^0.5.19, source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@@ -9653,7 +10317,7 @@ source-map-support@^0.5.19, source-map-support@~0.5.20:
buffer-from "^1.0.0"
source-map "^0.6.0"
-source-map@^0.5.0, source-map@~0.5.0:
+source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -9778,6 +10442,13 @@ ssri@^8.0.0, ssri@^8.0.1:
dependencies:
minipass "^3.1.1"
+stack-utils@^2.0.3:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
+ integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==
+ dependencies:
+ escape-string-regexp "^2.0.0"
+
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@@ -10698,7 +11369,7 @@ uuid@^7.0.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
-uuid@^8.0.0, uuid@^8.3.2:
+uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@@ -11157,7 +11828,7 @@ ws@^6.1.0:
dependencies:
async-limiter "~1.0.0"
-ws@^7.1.2:
+ws@^7.1.2, ws@^7.4.6:
version "7.5.6"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
@@ -11349,6 +12020,13 @@ yauzl@^2.10.0, yauzl@^2.4.2:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
+yazl@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35"
+ integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==
+ dependencies:
+ buffer-crc32 "~0.2.3"
+
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"