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"