deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
24
examples/playwright/.eslintrc.js
Normal file
24
examples/playwright/.eslintrc.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
ignorePatterns: ['playwright.config.ts'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json',
|
||||
// suppress warning from @typescript-eslint/typescript-estree plugin
|
||||
warnOnUnsupportedTypeScriptVersion: false
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
rules: {
|
||||
// override existing rules for playwright test package
|
||||
"no-null/no-null": "off",
|
||||
"no-undef": "off", // disabled due to 'browser', '$', '$$'
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
4
examples/playwright/.gitignore
vendored
Normal file
4
examples/playwright/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
allure-results
|
||||
test-results
|
||||
playwright-report
|
||||
.tmp.cfg
|
||||
54
examples/playwright/README.md
Normal file
54
examples/playwright/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - PLAYWRIGHT</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
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.
|
||||
|
||||
<div style='margin:0 auto;width:70%;'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
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/) 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)
|
||||
- [Theia 🎭 Playwright Template](https://github.com/eclipse-theia/theia-playwright-template)
|
||||
- [Building and Developing Theia 🎭 Playwright](./docs/DEVELOPING.md)
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
- [Playwright - GitHub](https://github.com/microsoft/playwright)
|
||||
- [Playwright - Website](https://playwright.dev)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
33
examples/playwright/configs/playwright.ci.config.ts
Normal file
33
examples/playwright/configs/playwright.ci.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 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 ciConfig: PlaywrightTestConfig = {
|
||||
...baseConfig,
|
||||
workers: 1,
|
||||
retries: 2,
|
||||
reporter: [
|
||||
['list'],
|
||||
['github'],
|
||||
['html', { open: 'never' }],
|
||||
],
|
||||
timeout: 30 * 1000, // Overwrite baseConfig timeout
|
||||
preserveOutput: 'always'
|
||||
};
|
||||
|
||||
export default ciConfig;
|
||||
45
examples/playwright/configs/playwright.config.ts
Normal file
45
examples/playwright/configs/playwright.config.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// *****************************************************************************
|
||||
// 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: 1,
|
||||
fullyParallel: false,
|
||||
// Timeout for each test in milliseconds.
|
||||
timeout: 60 * 1000,
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
browserName: 'chromium',
|
||||
permissions: ['clipboard-read'],
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
preserveOutput: 'failures-only',
|
||||
reporter: [
|
||||
['list'],
|
||||
['allure-playwright']
|
||||
],
|
||||
// Reuse Theia backend on port 3000 or start instance before executing the tests
|
||||
webServer: {
|
||||
command: 'npm run theia:start',
|
||||
port: 3000,
|
||||
reuseExistingServer: true
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
examples/playwright/configs/playwright.debug.config.ts
Normal file
27
examples/playwright/configs/playwright.debug.config.ts
Normal file
@@ -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;
|
||||
30
examples/playwright/configs/playwright.headful.config.ts
Normal file
30
examples/playwright/configs/playwright.headful.config.ts
Normal file
@@ -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;
|
||||
7
examples/playwright/configs/ui-tests.eslintrc.json
Normal file
7
examples/playwright/configs/ui-tests.eslintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
// override existing rules for ui-tests package
|
||||
"rules": {
|
||||
"no-undef": "off", // disabled due to 'browser', '$', '$$'
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
// override existing rules for ui-tests playwright package
|
||||
"rules": {
|
||||
"no-null/no-null": "off"
|
||||
}
|
||||
}
|
||||
57
examples/playwright/docs/DEVELOPING.md
Normal file
57
examples/playwright/docs/DEVELOPING.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Building and developing Theia 🎭 Playwright
|
||||
|
||||
## Building
|
||||
|
||||
Run `npm install && npm run build` in the root directory of the repository to build the Theia application.
|
||||
|
||||
In order to build Playwright library, the tests and install all dependencies (ex: chromium) run the build script:
|
||||
|
||||
```bash
|
||||
cd examples/playwright
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Executing the tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before running your tests, the Theia application under test needs to be running.
|
||||
|
||||
The Playwright configuration however is aware of that and starts the backend (`npm run theia:start`) on port 3000 if not already running.
|
||||
This is valid for executing tests with the VS Code Playwright extension or from your command line.
|
||||
|
||||
You may also use the `Launch Browser Backend` launch configuration in VS Code.
|
||||
|
||||
### Running the tests in VS Code via the Playwright extension
|
||||
|
||||
For quick and easy execution of tests in VS Code, we recommend using the [VS Code Playwright extension (`ms-playwright.playwright`)](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
|
||||
|
||||
Once you have installed the VS Code Playwright test extension, open the *Test* view and click the `Run Tests` button on the top toolbar or the `Run Test` button for a particular test.
|
||||
It uses the default configuration with chromium as test profile by default.
|
||||
|
||||
To run the tests headful, simply enable the checkbox `Show browser` in the Playwright section of the *Test* view.
|
||||
|
||||
### Running the tests headless via CLI
|
||||
|
||||
To start the tests run `npm run 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 `npm run ui-tests <path-to-file>` or `npm run ui-tests -g "<partial test file name>"`.
|
||||
See the [Playwright Test command line documentation](https://playwright.dev/docs/intro#command-line).
|
||||
|
||||
### Running the tests headful via CLI
|
||||
|
||||
If you want to observe the execution of the tests in a browser, use `npm run ui-tests-headful` for all tests or `npm run ui-tests-headful <path-to-file>` to only run a specific test.
|
||||
|
||||
### Watch the tests
|
||||
|
||||
Run `npm run watch` in the root of this package to rebuild the test code after each change.
|
||||
This ensures, that the executed tests are up-to-date also when running them with the [Playwright VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
|
||||
|
||||
### Debugging the tests
|
||||
|
||||
Please refer to the section [Debugging the tests via the VS Code Playwright extension](./GETTING_STARTED.md#debugging-the-tests-via-the-vs-code-playwright-extension).
|
||||
|
||||
### UI Mode - Watch and Trace Mode
|
||||
|
||||
Please refer to the section [UI Mode - Watch and Trace Mode](./GETTING_STARTED.md#ui-mode---watch-and-trace-mode).
|
||||
103
examples/playwright/docs/EXTENSIBILITY.md
Normal file
103
examples/playwright/docs/EXTENSIBILITY.md
Normal file
@@ -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 TheiaAppLoader.load({ playwright, browser });
|
||||
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<void> {
|
||||
await this.page.click(`${this.selector} .item1`);
|
||||
}
|
||||
}
|
||||
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1']);
|
||||
const app = await TheiaAppLoader.load({ playwright, browser }, ws, MyTheiaApp);
|
||||
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<void> {
|
||||
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 TheiaAppLoader.load({ playwright, browser });
|
||||
const myView = await app.openView(MyView);
|
||||
await myView.clickMyButton();
|
||||
```
|
||||
|
||||
A similar approach is used for custom editors. The only difference 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 TheiaAppLoader.load({ playwright, browser });
|
||||
const problemIndicator = await app.statusBar.statusIndicator(
|
||||
TheiaProblemIndicator
|
||||
);
|
||||
const numberOfProblems = await problemIndicator.numberOfProblems();
|
||||
expect(numberOfProblems).to.be(2);
|
||||
```
|
||||
184
examples/playwright/docs/GETTING_STARTED.md
Normal file
184
examples/playwright/docs/GETTING_STARTED.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Getting Started
|
||||
|
||||
The fastest way to getting started is to clone the [theia-playwright-template](https://github.com/eclipse-theia/theia-playwright-template) and build the theia-playwright-template.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:eclipse-theia/theia-playwright-template.git
|
||||
cd theia-playwright-template
|
||||
yarn
|
||||
```
|
||||
|
||||
The most important files in the theia-playwright-template are:
|
||||
|
||||
* Example test in `tests/theia-app.test.ts`
|
||||
* Example page object in `test/page-objects/theia-app.ts`
|
||||
* The base Playwright configuration file at `playwright.config.ts`
|
||||
* `package.json` with all required dependencies and scripts for running and debugging the tests
|
||||
|
||||
Now, let's run the tests:
|
||||
|
||||
1. Run your Theia application under test (not part of the theia-playwright-template)
|
||||
2. Run `yarn ui-tests` in the theia-playwright-template to run its tests in headless mode
|
||||
|
||||
Please note that Theia 🎭 Playwright is built to be extended with custom page objects, such as the one in `test/page-objects/theia-app.ts` in the theia-playwright-template.
|
||||
We recommend adding further page objects for all custom views, editors, widgets, etc.
|
||||
Please refer to the [extension guide](EXTENSIBILITY.md) for more information.
|
||||
|
||||
Moreover, this repository contains several tests based on Theia 🎭 Playwright in `examples/playwright/src/tests` that may serve as additional examples for writing tests.
|
||||
|
||||
## Adding further tests
|
||||
|
||||
Let's write another 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 ({ playwright, browser }) => {
|
||||
// 1. set up workspace contents and open Theia app
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, 'resources/sample-files1']);
|
||||
app = await TheiaAppLoader.load( { playwright, browser }, 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 VS Code debug tools.
|
||||
|
||||
<div style='margin:0 auto;width:100%;'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 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
|
||||
|
||||
## Building
|
||||
|
||||
Run `yarn` in the root directory of the repository to build the Theia application.
|
||||
|
||||
In order to build Playwright library, the tests and install all dependencies (ex: chromium) run the build script:
|
||||
|
||||
```bash
|
||||
cd examples/playwright
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Starting the Theia Application under test
|
||||
|
||||
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.
|
||||
|
||||
The Playwright configuration however is aware of that and starts the backend (`yarn theia:start`) on port 3000 if not already running.
|
||||
This is valid for executing tests with the VS Code Playwright extension or from your command line.
|
||||
|
||||
### Running the tests in VS Code via the Playwright extension
|
||||
|
||||
For quick and easy execution of tests in VS Code, we recommend using the [VS Code Playwright extension (`ms-playwright.playwright`)](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
|
||||
|
||||
Once you have installed the VS Code Playwright test extension, open the *Test* view and click the `Run Tests` button on the top toolbar or the `Run Test` button for a particular test.
|
||||
It uses the default configuration with chromium as test profile by default.
|
||||
|
||||
To run the tests headful, simply enable the checkbox `Show browser` in the Playwright section of the *Test* view.
|
||||
|
||||
### Running the tests headless via CLI
|
||||
|
||||
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 <path-to-file>` or `yarn ui-tests -g "<partial test file name>"`.
|
||||
See the [Playwright Test command line documentation](https://playwright.dev/docs/intro#command-line).
|
||||
|
||||
### Running the tests headful via CLI
|
||||
|
||||
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 <path-to-file>` to only run a specific test.
|
||||
|
||||
### Debugging the tests via the VS Code Playwright extension
|
||||
|
||||
To debug Playwright tests, open the *Test* view in VS Code and click the `Debug Tests` button on the top toolbar or the `Debug Test` for a particular test.
|
||||
It uses the default configuration with chromium as test profile by default.
|
||||
|
||||
For more information on debugging, please refer to the [Playwright documentation](https://playwright.dev/docs/debug).
|
||||
|
||||
### UI Mode - Watch and Trace Mode
|
||||
|
||||
For an advanced test development experience, Playwright provides the so-called *UI Mode*. To enable this, simply add the flag `--ui` to the CLI command.
|
||||
|
||||
```bash
|
||||
yarn ui-tests --ui
|
||||
```
|
||||
|
||||
For more information on the UI mode, please refer to the [Playwright announcement of the UI mode](https://playwright.dev/docs/release-notes#introducing-ui-mode-preview).
|
||||
|
||||
## 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).
|
||||
BIN
examples/playwright/docs/images/debug-example.gif
Normal file
BIN
examples/playwright/docs/images/debug-example.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 864 KiB |
BIN
examples/playwright/docs/images/teaser.gif
Normal file
BIN
examples/playwright/docs/images/teaser.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
53
examples/playwright/package.json
Normal file
53
examples/playwright/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@theia/playwright",
|
||||
"version": "1.68.0",
|
||||
"description": "System tests for Theia",
|
||||
"license": "EPL-2.0 OR GPL-2.0-only 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": {
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"build": "theiaext build && npm run playwright:install",
|
||||
"watch": "theiaext watch",
|
||||
"theia:start": "cd ../browser && rimraf .tmp.cfg && cross-env THEIA_CONFIG_DIR=$PWD/.tmp.cfg npm run start",
|
||||
"lint": "eslint -c ./.eslintrc.js --ext .ts ./src",
|
||||
"lint:fix": "eslint -c ./.eslintrc.js --ext .ts ./src --fix",
|
||||
"playwright:install": "playwright install chromium",
|
||||
"ui-tests": "npm run build && playwright test --config=./configs/playwright.config.ts",
|
||||
"ui-tests-electron": "npm run build && cross-env USE_ELECTRON=true playwright test --config=./configs/playwright.config.ts",
|
||||
"ui-tests-ci": "npm run build && playwright test --config=./configs/playwright.ci.config.ts",
|
||||
"ui-tests-headful": "npm run build && 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": "npm run ui-tests-report-generate && allure open allure-results/allure-report"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.47.0",
|
||||
"fs-extra": "^9.0.8",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/cli": "1.68.0",
|
||||
"@types/fs-extra": "^9.0.8",
|
||||
"allure-commandline": "^2.23.1",
|
||||
"allure-playwright": "^2.5.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "lib/index",
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
52
examples/playwright/src/index.ts
Normal file
52
examples/playwright/src/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './theia-about-dialog';
|
||||
export * from './theia-app';
|
||||
export * from './theia-app-loader';
|
||||
export * from './theia-context-menu';
|
||||
export * from './theia-dialog';
|
||||
export * from './theia-editor';
|
||||
export * from './theia-explorer-view';
|
||||
export * from './theia-main-menu';
|
||||
export * from './theia-menu-item';
|
||||
export * from './theia-menu';
|
||||
export * from './theia-monaco-editor';
|
||||
export * from './theia-notification-indicator';
|
||||
export * from './theia-notification-overlay';
|
||||
export * from './theia-notebook-cell';
|
||||
export * from './theia-notebook-editor';
|
||||
export * from './theia-notebook-toolbar';
|
||||
export * from './theia-output-channel';
|
||||
export * from './theia-output-view';
|
||||
export * from './theia-page-object';
|
||||
export * from './theia-preference-view';
|
||||
export * from './theia-problem-indicator';
|
||||
export * from './theia-problem-view';
|
||||
export * from './theia-quick-command-palette';
|
||||
export * from './theia-rename-dialog';
|
||||
export * from './theia-status-bar';
|
||||
export * from './theia-status-indicator';
|
||||
export * from './theia-terminal';
|
||||
export * from './theia-text-editor';
|
||||
export * from './theia-toggle-bottom-indicator';
|
||||
export * from './theia-toolbar';
|
||||
export * from './theia-toolbar-item';
|
||||
export * from './theia-tree-node';
|
||||
export * from './theia-view';
|
||||
export * from './theia-welcome-view';
|
||||
export * from './theia-workspace';
|
||||
export * from './util';
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"files.autoSave": "off"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
this is just a sample file
|
||||
content line 2
|
||||
content line 3
|
||||
content line 4
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just a sample file
|
||||
@@ -0,0 +1 @@
|
||||
this is just another sample file
|
||||
33
examples/playwright/src/tests/theia-app.test.ts
Normal file
33
examples/playwright/src/tests/theia-app.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
|
||||
test.describe('Theia Application', () => {
|
||||
let app: TheiaApp;
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should load and should show main content panel', async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
expect(await app.isMainContentPanelVisible()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Toro Cloud Pty Ltd 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-only WITH Classpath-exception-2.0
|
||||
|
||||
import { test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaExplorerView } from '../theia-explorer-view';
|
||||
import { TheiaTextEditor } from '../theia-text-editor';
|
||||
import { TheiaWelcomeView } from '../theia-welcome-view';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
test.describe('Theia Application Shell', () => {
|
||||
test.describe.configure({
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
let app: TheiaApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
|
||||
// The welcome view must be closed because the memory leak only occurs when there are
|
||||
// no tabs left open.
|
||||
const welcomeView = new TheiaWelcomeView(app);
|
||||
|
||||
if (await welcomeView.isTabVisible()) {
|
||||
await welcomeView.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* The aim of this test is to detect memory leaks when opening and closing editors many times.
|
||||
* Remove the skip and run the test, check the logs for any memory leak warnings.
|
||||
* It should take less than 2min to run, if it takes longer than that, just increase the timeout.
|
||||
*/
|
||||
test.skip('should open and close a text editor many times', async () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
|
||||
const fileStatNode = await explorer.getFileStatNodeByLabel('sample.txt');
|
||||
const contextMenu = await fileStatNode.openContextMenu();
|
||||
await contextMenu.clickMenuItem('Open');
|
||||
|
||||
const textEditor = new TheiaTextEditor('sample.txt', app);
|
||||
await textEditor.waitForVisible();
|
||||
|
||||
await textEditor.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
213
examples/playwright/src/tests/theia-explorer-view.test.ts
Normal file
213
examples/playwright/src/tests/theia-explorer-view.test.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { PreferenceIds, TheiaPreferenceView } from '../theia-preference-view';
|
||||
import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
test.describe('Theia Explorer View', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let explorer: TheiaExplorerView;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
|
||||
if (app.isElectron) {
|
||||
// set trash preference to off
|
||||
const preferenceView = await app.openPreferences(TheiaPreferenceView);
|
||||
await preferenceView.setBooleanPreferenceById(PreferenceIds.Files.EnableTrash, false);
|
||||
await preferenceView.close();
|
||||
}
|
||||
|
||||
explorer = await app.openView(TheiaExplorerView);
|
||||
await explorer.waitForVisibleFileNodes();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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", one named "sampleFolderCompact" 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(3);
|
||||
|
||||
let file; let folder; let compactFolder;
|
||||
if (await fileStatElements[0].isFolder()) {
|
||||
folder = fileStatElements[0];
|
||||
compactFolder = fileStatElements[1];
|
||||
file = fileStatElements[2];
|
||||
} else {
|
||||
folder = fileStatElements[2];
|
||||
compactFolder = 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 compactFolder.label()).toBe('sampleFolderCompact');
|
||||
expect(await compactFolder.isFile()).toBe(false);
|
||||
expect(await compactFolder.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 and collapsed', 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);
|
||||
|
||||
await folder.collapse();
|
||||
expect(await folder.isCollapsed()).toBe(true);
|
||||
});
|
||||
|
||||
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 be able to check if compact folder "sampleFolderCompact/nestedFolder1/nestedFolder2" exists', async () => {
|
||||
const fileStatElements = await explorer.visibleFileStatNodes();
|
||||
// default setting `explorer.compactFolders=true` renders folders in a compact form - single child folders will be compressed in a combined tree element
|
||||
expect(await explorer.existsDirectoryNode('sampleFolderCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
|
||||
// the `existsDirectoryNode` function will expand the folder, hence we wait for the file nodes to increase as we expect a txt child file node
|
||||
await explorer.waitForFileNodesToIncrease(fileStatElements.length);
|
||||
});
|
||||
|
||||
test('should provide file stat node by path of compact folder "sampleFolderCompact/nestedFolder1/nestedFolder2/sampleFile1-1.txt"', async () => {
|
||||
const file = await explorer.fileStatNode('sampleFolderCompact/nestedFolder1/nestedFolder2/sampleFile1-1.txt', true /* compact */);
|
||||
if (!file) { throw Error('File stat node could not be retrieved by path'); }
|
||||
expect(await file.label()).toBe('sampleFile1-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');
|
||||
if (!app.isElectron) {
|
||||
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);
|
||||
});
|
||||
|
||||
test('should open context menu on nested folder segment "nestedFolder1"', async () => {
|
||||
expect(await explorer.existsDirectoryNode('sampleFolderCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
|
||||
const folder = await explorer.getFileStatNodeByLabel('sampleFolderCompact/nestedFolder1/nestedFolder2', true /* compact */);
|
||||
const menu = await folder.openContextMenuOnSegment('nestedFolder1');
|
||||
expect(await menu.isOpen()).toBe(true);
|
||||
|
||||
const menuItems = await menu.visibleMenuItems();
|
||||
expect(menuItems).toContain('New File...');
|
||||
expect(menuItems).toContain('New Folder...');
|
||||
expect(menuItems).toContain('Open in Integrated Terminal');
|
||||
expect(menuItems).toContain('Find in Folder...');
|
||||
|
||||
await menu.close();
|
||||
expect(await menu.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('should rename compact folder "sampleFolderCompact" to "sampleDirectoryCompact', async () => {
|
||||
expect(await explorer.existsDirectoryNode('sampleFolderCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
|
||||
await explorer.renameNode(
|
||||
'sampleFolderCompact/nestedFolder1/nestedFolder2', 'sampleDirectoryCompact',
|
||||
true /* confirm */, 'sampleFolderCompact' /* nodeSegmentLabel */);
|
||||
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
|
||||
});
|
||||
|
||||
// TODO These tests only seems to fail on Ubuntu - it's not clear why
|
||||
test.skip('should delete nested folder "sampleDirectoryCompact/nestedFolder1/nestedFolder2"', async () => {
|
||||
const fileStatElements = await explorer.visibleFileStatNodes();
|
||||
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
|
||||
await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* confirm */, 'nestedFolder2' /* nodeSegmentLabel */);
|
||||
await explorer.waitForFileNodesToDecrease(fileStatElements.length);
|
||||
const updatedFileStatElements = await explorer.visibleFileStatNodes();
|
||||
expect(updatedFileStatElements.length).toBe(fileStatElements.length - 1);
|
||||
});
|
||||
|
||||
test.skip('should delete compact folder "sampleDirectoryCompact/nestedFolder1"', async () => {
|
||||
const fileStatElements = await explorer.visibleFileStatNodes();
|
||||
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1', true /* compact */)).toBe(true);
|
||||
await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1', true /* confirm */, 'sampleDirectoryCompact' /* nodeSegmentLabel */);
|
||||
await explorer.waitForFileNodesToDecrease(fileStatElements.length);
|
||||
const updatedFileStatElements = await explorer.visibleFileStatNodes();
|
||||
expect(updatedFileStatElements.length).toBe(fileStatElements.length - 1);
|
||||
});
|
||||
|
||||
test('open "sample.txt" via the context menu', async () => {
|
||||
expect(await explorer.existsFileNode('sample.txt')).toBe(true);
|
||||
await explorer.clickContextMenuItem('sample.txt', ['Open']);
|
||||
const span = await app.page.waitForSelector('span:has-text("content line 2")');
|
||||
expect(await span.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
50
examples/playwright/src/tests/theia-getting-started.test.ts
Normal file
50
examples/playwright/src/tests/theia-getting-started.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaExplorerView } from '../theia-explorer-view';
|
||||
|
||||
/**
|
||||
* Test the Theia welcome page from the getting-started package.
|
||||
*/
|
||||
test.describe('Theia Welcome Page', () => {
|
||||
let app: TheiaApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
await app.isMainContentPanelVisible();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('New File... entry should create a new file.', async () => {
|
||||
await app.page.getByRole('button', { name: 'New File...' }).click();
|
||||
const quickPicker = app.page.getByPlaceholder('Select File Type or Enter');
|
||||
await quickPicker.fill('testfile.txt');
|
||||
await quickPicker.press('Enter');
|
||||
await app.page.getByRole('button', { name: 'Create File' }).click();
|
||||
|
||||
// check file in workspace exists
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
await explorer.refresh();
|
||||
await explorer.waitForVisibleFileNodes();
|
||||
expect(await explorer.existsFileNode('testfile.txt')).toBe(true);
|
||||
});
|
||||
});
|
||||
132
examples/playwright/src/tests/theia-main-menu.test.ts
Normal file
132
examples/playwright/src/tests/theia-main-menu.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaAboutDialog } from '../theia-about-dialog';
|
||||
import { TheiaMenuBar } from '../theia-main-menu';
|
||||
import { OSUtil } from '../util';
|
||||
import { TheiaExplorerView } from '../theia-explorer-view';
|
||||
|
||||
test.describe('Theia Main Menu', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let menuBar: TheiaMenuBar;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
menuBar = app.menuBar;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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 Text File' and 'New Folder'", async () => {
|
||||
const mainMenu = await menuBar.openMenu('File');
|
||||
const menuItems = await mainMenu.visibleMenuItems();
|
||||
expect(menuItems).toContain('New Text File');
|
||||
expect(menuItems).toContain('New Folder...');
|
||||
});
|
||||
|
||||
test("should return menu item by name 'New Text File'", async () => {
|
||||
const mainMenu = await menuBar.openMenu('File');
|
||||
const menuItem = await mainMenu.menuItemByName('New Text File');
|
||||
expect(menuItem).toBeDefined();
|
||||
|
||||
const label = await menuItem?.label();
|
||||
expect(label).toBe('New Text File');
|
||||
|
||||
const shortCut = await menuItem?.shortCut();
|
||||
expect(shortCut).toBe(OSUtil.isMacOS ? '⌥ N' : app.isElectron ? 'Ctrl+N' : '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 Text 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', 'Settings');
|
||||
|
||||
const label = await openPreferencesItem?.label();
|
||||
expect(label).toBe('Settings');
|
||||
});
|
||||
|
||||
test('should close main menu', async () => {
|
||||
const mainMenu = await menuBar.openMenu('File');
|
||||
await mainMenu.close();
|
||||
expect(await mainMenu.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('open about dialog using menu', async () => {
|
||||
await (await menuBar.openMenu('Help')).clickMenuItem('About');
|
||||
const aboutDialog = new TheiaAboutDialog(app);
|
||||
expect(await aboutDialog.isVisible()).toBe(true);
|
||||
await aboutDialog.page.locator('#theia-dialog-shell').getByRole('button', { name: 'OK' }).click();
|
||||
expect(await aboutDialog.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('open file via file menu and cancel', async () => {
|
||||
const openFileEntry = app.isElectron ? 'Open File...' : 'Open...';
|
||||
await (await menuBar.openMenu('File')).clickMenuItem(openFileEntry);
|
||||
const fileDialog = await app.page.waitForSelector('div[class="dialogBlock"]');
|
||||
expect(await fileDialog.isVisible()).toBe(true);
|
||||
await app.page.locator('#theia-dialog-shell').getByRole('button', { name: 'Cancel' }).click();
|
||||
expect(await fileDialog.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('Create file via New File menu and accept', async () => {
|
||||
await (await menuBar.openMenu('File')).clickMenuItem('New File...');
|
||||
const quickPick = app.page.getByPlaceholder('Select File Type or Enter');
|
||||
// type file name and press enter
|
||||
await quickPick.fill('test.txt');
|
||||
await quickPick.press('Enter');
|
||||
|
||||
// check file dialog is opened and accept with ENTER
|
||||
const fileDialog = await app.page.waitForSelector('div[class="dialogBlock"]');
|
||||
expect(await fileDialog.isVisible()).toBe(true);
|
||||
await app.page.locator('#theia-dialog-shell').press('Enter');
|
||||
expect(await fileDialog.isVisible()).toBe(false);
|
||||
|
||||
// check file in workspace exists
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
await explorer.refresh();
|
||||
await explorer.waitForVisibleFileNodes();
|
||||
expect(await explorer.existsFileNode('test.txt')).toBe(true);
|
||||
});
|
||||
});
|
||||
365
examples/playwright/src/tests/theia-notebook-editor.test.ts
Normal file
365
examples/playwright/src/tests/theia-notebook-editor.test.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Locator, PlaywrightWorkerArgs, expect, test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader, TheiaPlaywrightTestConfig } from '../theia-app-loader';
|
||||
import { TheiaNotebookCell } from '../theia-notebook-cell';
|
||||
import { TheiaNotebookEditor } from '../theia-notebook-editor';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
// See .github/workflows/playwright.yml for preferred python version
|
||||
const preferredKernel = process.env.CI ? 'Python 3.13' : 'Python 3';
|
||||
|
||||
async function ensureKernelSelected(editor: TheiaNotebookEditor): Promise<void> {
|
||||
const selectedKernel = await editor.selectedKernel();
|
||||
if (selectedKernel?.match(new RegExp(`^${preferredKernel}`)) === null) {
|
||||
await editor.selectKernel(preferredKernel);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeEditorWithoutSave(editor: TheiaNotebookEditor): Promise<void> {
|
||||
if (await editor.isDirty()) {
|
||||
await editor.closeWithoutSave();
|
||||
} else {
|
||||
await editor.close();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Python Kernel Installed', () => {
|
||||
let app: TheiaApp;
|
||||
let editor: TheiaNotebookEditor;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await loadApp({ playwright, browser });
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (app.page) {
|
||||
await app.page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await closeEditorWithoutSave(editor);
|
||||
});
|
||||
|
||||
test('kernels are installed', async () => {
|
||||
const kernels = await editor.availableKernels();
|
||||
const msg = `Available kernels:\n ${kernels.join('\n')}`;
|
||||
console.log(msg); // Print available kernels, useful when running in CI.
|
||||
expect(kernels.length, msg).toBeGreaterThan(0);
|
||||
|
||||
const py3kernel = kernels.filter(kernel => kernel.match(new RegExp(`^${preferredKernel}`)));
|
||||
expect(py3kernel.length, msg).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should select a kernel', async () => {
|
||||
await editor.selectKernel(preferredKernel);
|
||||
const selectedKernel = await editor.selectedKernel();
|
||||
expect(selectedKernel).toMatch(new RegExp(`^${preferredKernel}`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Theia Notebook Editor interaction', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let editor: TheiaNotebookEditor;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await loadApp({ playwright, browser });
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
|
||||
await ensureKernelSelected(editor);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (app.page) {
|
||||
await app.page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await closeEditorWithoutSave(editor);
|
||||
});
|
||||
|
||||
test('should add a new code cell', async () => {
|
||||
await editor.addCodeCell();
|
||||
const cells = await editor.cells();
|
||||
expect(cells.length).toBe(2);
|
||||
expect(await cells[1].mode()).toBe('python');
|
||||
});
|
||||
|
||||
test('should add a new markdown cell', async () => {
|
||||
await editor.addMarkdownCell();
|
||||
await (await editor.cells())[1].addEditorText('print("markdown")');
|
||||
|
||||
const cells = await editor.cells();
|
||||
expect(cells.length).toBe(2);
|
||||
expect(await cells[1].mode()).toBe('markdown');
|
||||
expect(await cells[1].editorText()).toBe('print("markdown")');
|
||||
});
|
||||
|
||||
test('should execute all cells', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("Hallo Notebook!")');
|
||||
|
||||
await editor.addCodeCell();
|
||||
const secondCell = (await editor.cells())[1];
|
||||
await secondCell.addEditorText('print("Bye Notebook!")');
|
||||
|
||||
await editor.executeAllCells();
|
||||
|
||||
expect(await cell.outputText()).toBe('Hallo Notebook!');
|
||||
expect(await secondCell.outputText()).toBe('Bye Notebook!');
|
||||
});
|
||||
|
||||
test('should split cell', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
/*
|
||||
Add cell text:
|
||||
print("Line-1")
|
||||
print("Line-2")
|
||||
*/
|
||||
await cell.addEditorText('print("Line-1")\nprint("Line-2")');
|
||||
|
||||
/*
|
||||
Set cursor:
|
||||
print("Line-1")
|
||||
<|>print("Line-2")
|
||||
*/
|
||||
const line = await cell.editor.monacoEditor.line(1);
|
||||
expect(line, { message: 'Line number 1 should exists' }).toBeDefined();
|
||||
await line!.click();
|
||||
await line!.press('ArrowRight');
|
||||
|
||||
// split cell
|
||||
await cell.splitCell();
|
||||
|
||||
// expect two cells with text "print("Line-1")" and "print("Line-2")"
|
||||
expect(await editor.cells()).toHaveLength(2);
|
||||
expect(await (await editor.cells())[0].editorText()).toBe('print("Line-1")');
|
||||
expect(await (await editor.cells())[1].editorText()).toBe('print("Line-2")');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Theia Notebook Cell interaction', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let editor: TheiaNotebookEditor;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await loadApp({ playwright, browser });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (app.page) {
|
||||
await app.page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
|
||||
await ensureKernelSelected(editor);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await closeEditorWithoutSave(editor);
|
||||
});
|
||||
|
||||
test('should write text in a code cell', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
// assume the first cell is a code cell
|
||||
expect(await cell.isCodeCell()).toBe(true);
|
||||
|
||||
await cell.addEditorText('print("Hallo")');
|
||||
const cellText = await cell.editorText();
|
||||
expect(cellText).toBe('print("Hallo")');
|
||||
});
|
||||
|
||||
test('should write multi-line text in a code cell', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("Hallo")\nprint("Notebook")');
|
||||
|
||||
const cellText = await cell.editorText();
|
||||
expect(cellText).toBe('print("Hallo")\nprint("Notebook")');
|
||||
});
|
||||
|
||||
test('Execute code cell and read output', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("Hallo Notebook!")');
|
||||
await cell.execute();
|
||||
|
||||
const cellOutput = await cell.outputText();
|
||||
expect(cellOutput).toBe('Hallo Notebook!');
|
||||
});
|
||||
|
||||
test('Check execution count matches', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("Hallo Notebook!")');
|
||||
await cell.execute();
|
||||
await cell.execute();
|
||||
await cell.execute();
|
||||
|
||||
expect(await cell.executionCount()).toBe('3');
|
||||
});
|
||||
|
||||
test('Check arrow up and down works', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await editor.addCodeCell();
|
||||
const secondCell = (await editor.cells())[1];
|
||||
// second cell is selected after creation
|
||||
expect(await secondCell.isSelected()).toBe(true);
|
||||
// select cell above
|
||||
await editor.page.keyboard.type('second cell');
|
||||
await secondCell.editor.page.keyboard.press('ArrowUp');
|
||||
expect(await cell.isSelected()).toBe(true);
|
||||
|
||||
// select cell below
|
||||
await cell.app.page.keyboard.press('ArrowDown');
|
||||
expect(await secondCell.isSelected()).toBe(true);
|
||||
});
|
||||
|
||||
test('Check k(up)/j(down) selection works', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await editor.addCodeCell();
|
||||
const secondCell = (await editor.cells())[1];
|
||||
// second cell is selected after creation
|
||||
expect(await secondCell.isSelected()).toBe(true);
|
||||
// deselect editor focus and focus the whole cell
|
||||
await secondCell.selectCell();
|
||||
|
||||
// select cell above
|
||||
await secondCell.editor.page.keyboard.press('k');
|
||||
expect(await cell.isSelected()).toBe(true);
|
||||
|
||||
// select cell below
|
||||
await cell.app.page.keyboard.press('j');
|
||||
expect(await secondCell.isSelected()).toBe(true);
|
||||
});
|
||||
|
||||
test('Check x/c/v works', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("First cell")');
|
||||
|
||||
// add and fill second cell
|
||||
await editor.addCodeCell();
|
||||
// TODO workaround for create command bug.
|
||||
// The first time created cell doesn't contain a monaco-editor child div.
|
||||
await ((await editor.cells())[1]).deleteCell();
|
||||
await editor.addCodeCell();
|
||||
|
||||
const secondCell = (await editor.cells())[1];
|
||||
await secondCell.locator.waitFor({ state: 'visible' });
|
||||
await secondCell.addEditorText('print("Second cell")');
|
||||
await secondCell.selectCell(); // deselect editor focus
|
||||
|
||||
// cut second cell
|
||||
await secondCell.page.keyboard.press('x');
|
||||
await editor.waitForCellCountChanged(2);
|
||||
expect((await editor.cells()).length).toBe(1);
|
||||
|
||||
// paste second cell
|
||||
await cell.selectCell();
|
||||
await cell.page.keyboard.press('v');
|
||||
await editor.waitForCellCountChanged(1);
|
||||
expect((await editor.cells()).length).toBe(2);
|
||||
const pastedCell = (await editor.cells())[1];
|
||||
expect(await pastedCell.isSelected()).toBe(true);
|
||||
|
||||
// copy first cell
|
||||
await cell.selectCell(); // deselect editor focus
|
||||
await cell.page.keyboard.press('c');
|
||||
// paste copied cell
|
||||
await cell.page.keyboard.press('v');
|
||||
await editor.waitForCellCountChanged(2);
|
||||
expect((await editor.cells()).length).toBe(3);
|
||||
expect(await (await editor.cells())[0].editorText()).toBe('print("First cell")');
|
||||
expect(await (await editor.cells())[1].editorText()).toBe('print("First cell")');
|
||||
expect(await (await editor.cells())[2].editorText()).toBe('print("Second cell")');
|
||||
expect(await editor.isDirty()).toBe(true); // ensure editor is dirty after copy/paste
|
||||
});
|
||||
|
||||
test('Check LineNumber switch `l` works', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("First cell")');
|
||||
await cell.selectCell();
|
||||
await cell.page.keyboard.press('l');
|
||||
// NOTE: div.line-numbers is not visible
|
||||
await cell.editor.locator.locator('.overflow-guard > div.line-numbers').waitFor({ state: 'attached' });
|
||||
});
|
||||
|
||||
test('Check Collapse output switch `o` works', async () => {
|
||||
const cell = await firstCell(editor);
|
||||
await cell.addEditorText('print("Check output collapse")');
|
||||
await cell.selectCell();
|
||||
await cell.execute(); // produce output
|
||||
expect(await cell.outputText()).toBe('Check output collapse');
|
||||
|
||||
await cell.page.keyboard.press('o');
|
||||
await (await cell.outputContainer()).waitFor({ state: 'hidden' });
|
||||
await cell.page.keyboard.press('o');
|
||||
await (await cell.outputContainer()).waitFor({ state: 'visible' });
|
||||
|
||||
expect(await cell.outputText()).toBe('Check output collapse');
|
||||
});
|
||||
|
||||
test('Check arrow-up/arrow-down/escape with code completion', async () => {
|
||||
await editor.addMarkdownCell();
|
||||
const mdCell = (await editor.cells())[1];
|
||||
await mdCell.addEditorText('h');
|
||||
|
||||
await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true)
|
||||
await ensureCodeCompletionVisible(mdCell.editor.locator);
|
||||
await editor.page.keyboard.press('Escape'); // close CC
|
||||
// check the same cell still selected and not lose the edit mode
|
||||
expect(await mdCell.editor.monacoEditor.isFocused()).toBe(true);
|
||||
|
||||
await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true)
|
||||
await ensureCodeCompletionVisible(mdCell.editor.locator);
|
||||
await editor.page.keyboard.press('ArrowUp'); // select next entry in CC list
|
||||
await editor.page.keyboard.press('Enter'); // apply completion
|
||||
// check the same cell still selected and not the second one due to 'ArrowDown' being pressed
|
||||
expect(await mdCell.isSelected()).toBe(true);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
async function ensureCodeCompletionVisible(parent: Locator): Promise<void> {
|
||||
await parent.locator('div.monaco-editor div.suggest-widget').waitFor({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async function firstCell(editor: TheiaNotebookEditor): Promise<TheiaNotebookCell> {
|
||||
return (await editor.cells())[0];
|
||||
}
|
||||
|
||||
async function loadApp(args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs): Promise<TheiaApp> {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/notebook-files')]);
|
||||
const app = await TheiaAppLoader.load(args, ws);
|
||||
// auto-save are disabled using settings.json file
|
||||
// see examples/playwright/src/tests/resources/notebook-files/.theia/settings.json
|
||||
|
||||
// NOTE: Workspace trust is disabled in examples/browser/package.json using default preferences.
|
||||
// If workspace trust check is on, python extension will not be able to explore Python installations.
|
||||
return app;
|
||||
}
|
||||
85
examples/playwright/src/tests/theia-output-view.test.ts
Normal file
85
examples/playwright/src/tests/theia-output-view.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaOutputViewChannel } from '../theia-output-channel';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaOutputView } from '../theia-output-view';
|
||||
|
||||
let app: TheiaApp; let outputView: TheiaOutputView; let testChannel: TheiaOutputViewChannel;
|
||||
test.describe('Theia Output View', () => {
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should open the output view and check if is visible and active', async () => {
|
||||
outputView = await app.openView(TheiaOutputView);
|
||||
expect(await outputView.isTabVisible()).toBe(true);
|
||||
expect(await outputView.isDisplayed()).toBe(true);
|
||||
expect(await outputView.isActive()).toBe(true);
|
||||
});
|
||||
test('should be opened at the bottom and have the title "Output"', async () => {
|
||||
expect(await outputView.isInSidePanel()).toBe(false);
|
||||
expect(await outputView.side()).toBe('bottom');
|
||||
expect(await outputView.title()).toBe('Output');
|
||||
});
|
||||
test('should be closable', async () => {
|
||||
expect(await outputView.isClosable()).toBe(true);
|
||||
await outputView.close();
|
||||
expect(await outputView.isTabVisible()).toBe(false);
|
||||
expect(await outputView.isDisplayed()).toBe(false);
|
||||
expect(await outputView.isActive()).toBe(false);
|
||||
});
|
||||
test('should select a test output channel', async () => {
|
||||
outputView = await app.openView(TheiaOutputView);
|
||||
expect(await outputView.isTabVisible()).toBe(true);
|
||||
expect(await outputView.isDisplayed()).toBe(true);
|
||||
expect(await outputView.isActive()).toBe(true);
|
||||
|
||||
const testChannelName = 'API Sample: my test channel';
|
||||
expect(await outputView.selectOutputChannel(testChannelName)).toBe(true);
|
||||
});
|
||||
test('should check if the output view of the test output channel', async () => {
|
||||
const testChannelName = 'API Sample: my test channel';
|
||||
expect(await outputView.isOutputChannelSelected(testChannelName));
|
||||
const channel = await outputView.getOutputChannel(testChannelName);
|
||||
expect(channel).toBeDefined;
|
||||
testChannel = channel!;
|
||||
expect(await testChannel!.isDisplayed()).toBe(true);
|
||||
});
|
||||
test('should check if the output view test channel shows the test output', async () => {
|
||||
expect(await testChannel.numberOfLines()).toBe(5);
|
||||
expect(await testChannel.textContentOfLineByLineNumber(1)).toMatch('hello info1');
|
||||
expect(await testChannel.maxSeverityOfLineByLineNumber(1)).toMatch('info');
|
||||
expect(await testChannel.textContentOfLineByLineNumber(2)).toMatch('hello info2');
|
||||
expect(await testChannel.maxSeverityOfLineByLineNumber(2)).toMatch('info');
|
||||
expect(await testChannel.textContentOfLineByLineNumber(3)).toMatch('hello error');
|
||||
expect(await testChannel.maxSeverityOfLineByLineNumber(3)).toMatch('error');
|
||||
expect(await testChannel.textContentOfLineByLineNumber(4)).toMatch('hello warning');
|
||||
expect(await testChannel.maxSeverityOfLineByLineNumber(4)).toMatch('warning');
|
||||
expect(await testChannel.textContentOfLineByLineNumber(5)).toMatch(
|
||||
'inlineInfo1 inlineWarning inlineError inlineInfo2'
|
||||
);
|
||||
expect(await testChannel.maxSeverityOfLineByLineNumber(5)).toMatch('error');
|
||||
});
|
||||
|
||||
});
|
||||
122
examples/playwright/src/tests/theia-preference-view.test.ts
Normal file
122
examples/playwright/src/tests/theia-preference-view.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view';
|
||||
|
||||
test.describe('Preference View', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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.resetPreferenceById(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.resetPreferenceById(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.resetPreferenceById(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.resetPreferenceById(preferenceId);
|
||||
expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(DefaultPreferences.Explorer.AutoReveal.Enabled);
|
||||
});
|
||||
|
||||
test('should be able to read, set, and reset Options preferences', async () => {
|
||||
const preferences = await app.openPreferences(TheiaPreferenceView);
|
||||
const preferenceId = PreferenceIds.Editor.RenderWhitespace;
|
||||
|
||||
await preferences.resetPreferenceById(preferenceId);
|
||||
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Selection);
|
||||
|
||||
await preferences.setOptionsPreferenceById(preferenceId, DefaultPreferences.Editor.RenderWhitespace.Boundary);
|
||||
await preferences.waitForModified(preferenceId);
|
||||
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Boundary);
|
||||
|
||||
await preferences.resetPreferenceById(preferenceId);
|
||||
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Selection);
|
||||
});
|
||||
|
||||
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('not.a.real.preference')).rejects.toThrowError();
|
||||
await expect(preferences.setBooleanPreferenceById('not.a.real.preference', true)).rejects.toThrowError();
|
||||
await expect(preferences.resetPreferenceById('not.a.real.preference')).rejects.toThrowError();
|
||||
|
||||
await expect(preferences.getStringPreferenceById('not.a.real.preference')).rejects.toThrowError();
|
||||
await expect(preferences.setStringPreferenceById('not.a.real.preference', 'a')).rejects.toThrowError();
|
||||
await expect(preferences.resetPreferenceById('not.a.real.preference')).rejects.toThrowError();
|
||||
|
||||
await expect(preferences.getOptionsPreferenceById('not.a.real.preference')).rejects.toThrowError();
|
||||
await expect(preferences.setOptionsPreferenceById('not.a.real.preference', 'a')).rejects.toThrowError();
|
||||
await expect(preferences.resetPreferenceById('not.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;
|
||||
|
||||
preferences.customTimeout = 500;
|
||||
try {
|
||||
await expect(preferences.getBooleanPreferenceById(stringPreference)).rejects.toThrowError();
|
||||
await expect(preferences.setBooleanPreferenceById(stringPreference, true)).rejects.toThrowError();
|
||||
await expect(preferences.setStringPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
|
||||
await expect(preferences.setOptionsPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
|
||||
} finally {
|
||||
preferences.customTimeout = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
64
examples/playwright/src/tests/theia-problems-view.test.ts
Normal file
64
examples/playwright/src/tests/theia-problems-view.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaProblemsView } from '../theia-problem-view';
|
||||
|
||||
test.describe('Theia Problems View', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
86
examples/playwright/src/tests/theia-quick-command.test.ts
Normal file
86
examples/playwright/src/tests/theia-quick-command.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaAboutDialog } from '../theia-about-dialog';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaExplorerView } from '../theia-explorer-view';
|
||||
import { TheiaNotificationIndicator } from '../theia-notification-indicator';
|
||||
import { TheiaNotificationOverlay } from '../theia-notification-overlay';
|
||||
import { TheiaQuickCommandPalette } from '../theia-quick-command-palette';
|
||||
|
||||
test.describe('Theia Quick Command', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let quickCommand: TheiaQuickCommandPalette;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
quickCommand = app.quickCommandPalette;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should show quick command palette', async () => {
|
||||
await quickCommand.open();
|
||||
expect(await quickCommand.isOpen()).toBe(true);
|
||||
await quickCommand.hide();
|
||||
expect(await quickCommand.isOpen()).toBe(false);
|
||||
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 Theia');
|
||||
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 Exp');
|
||||
await quickCommand.trigger('View: Toggle Explorer');
|
||||
expect(await quickCommand.isOpen()).toBe(false);
|
||||
const explorerView = new TheiaExplorerView(app);
|
||||
expect(await explorerView.isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
test('should trigger \'Quick Input: Test Positive Integer\' command by confirming via Enter', async () => {
|
||||
await quickCommand.type('Test Positive', true);
|
||||
expect(await quickCommand.isOpen()).toBe(true);
|
||||
await quickCommand.type('6', true);
|
||||
const notificationIndicator = new TheiaNotificationIndicator(app);
|
||||
const notification = new TheiaNotificationOverlay(app, notificationIndicator);
|
||||
expect(await notification.isEntryVisible('Positive Integer: 6')).toBe(true);
|
||||
});
|
||||
|
||||
test('retrieve and check visible items', async () => {
|
||||
await quickCommand.type('close all tabs', false);
|
||||
const listItems = await Promise.all((await quickCommand.visibleItems()).map(async item => item.textContent()));
|
||||
expect(listItems).toContain('View: Close All Tabs in Main Area');
|
||||
});
|
||||
|
||||
});
|
||||
66
examples/playwright/src/tests/theia-sample-app.test.ts
Normal file
66
examples/playwright/src/tests/theia-sample-app.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaToolbar } from '../theia-toolbar';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
class TheiaSampleApp extends TheiaApp {
|
||||
protected toolbar = new TheiaToolbar(this);
|
||||
|
||||
override async waitForInitialized(): Promise<void> {
|
||||
await this.toolbar.show();
|
||||
}
|
||||
|
||||
async toggleToolbar(): Promise<void> {
|
||||
await this.toolbar.toggle();
|
||||
}
|
||||
|
||||
async isToolbarVisible(): Promise<boolean> {
|
||||
return this.toolbar.isShown();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Theia Sample Application', () => {
|
||||
|
||||
let app: TheiaSampleApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser }, new TheiaWorkspace(), TheiaSampleApp);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should start with visible toolbar', async () => {
|
||||
expect(await app.isToolbarVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should toggle toolbar', async () => {
|
||||
await app.toggleToolbar();
|
||||
expect(await app.isToolbarVisible()).toBe(false);
|
||||
|
||||
await app.toggleToolbar();
|
||||
expect(await app.isToolbarVisible()).toBe(true);
|
||||
|
||||
await app.toggleToolbar();
|
||||
expect(await app.isToolbarVisible()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
52
examples/playwright/src/tests/theia-status-bar.test.ts
Normal file
52
examples/playwright/src/tests/theia-status-bar.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaNotificationIndicator } from '../theia-notification-indicator';
|
||||
import { TheiaProblemIndicator } from '../theia-problem-indicator';
|
||||
import { TheiaStatusBar } from '../theia-status-bar';
|
||||
import { TheiaToggleBottomIndicator } from '../theia-toggle-bottom-indicator';
|
||||
|
||||
test.describe('Theia Status Bar', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
let statusBar: TheiaStatusBar;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
statusBar = app.statusBar;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
91
examples/playwright/src/tests/theia-terminal-view.test.ts
Normal file
91
examples/playwright/src/tests/theia-terminal-view.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
import { TheiaTerminal } from '../theia-terminal';
|
||||
|
||||
let app: TheiaApp;
|
||||
|
||||
test.describe('Theia Terminal View', () => {
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should be possible to open a new terminal', async () => {
|
||||
const terminal = await app.openTerminal(TheiaTerminal);
|
||||
expect(await terminal.isTabVisible()).toBe(true);
|
||||
expect(await terminal.isDisplayed()).toBe(true);
|
||||
expect(await terminal.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
test('should be possible to open two terminals, switch among them, and close them', async () => {
|
||||
const terminal1 = await app.openTerminal(TheiaTerminal);
|
||||
const terminal2 = await app.openTerminal(TheiaTerminal);
|
||||
const allTerminals = [terminal1, terminal2];
|
||||
|
||||
// all terminal tabs should be visible
|
||||
for (const terminal of allTerminals) {
|
||||
expect(await terminal.isTabVisible()).toBe(true);
|
||||
}
|
||||
|
||||
// activate one terminal after the other and check that only this terminal is active
|
||||
for (const terminal of allTerminals) {
|
||||
await terminal.activate();
|
||||
expect(await terminal1.isActive()).toBe(terminal1 === terminal);
|
||||
expect(await terminal2.isActive()).toBe(terminal2 === terminal);
|
||||
}
|
||||
|
||||
// close all terminals
|
||||
for (const terminal of allTerminals) {
|
||||
await terminal.activate();
|
||||
await terminal.close();
|
||||
}
|
||||
|
||||
// check that all terminals are closed
|
||||
for (const terminal of allTerminals) {
|
||||
expect(await terminal.isTabVisible()).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow to write and read terminal contents', async () => {
|
||||
const terminal = await app.openTerminal(TheiaTerminal);
|
||||
await terminal.write('hello');
|
||||
const contents = await terminal.contents();
|
||||
expect(contents).toContain('hello');
|
||||
});
|
||||
|
||||
test('should allow to submit a command and read output', async () => {
|
||||
const terminal = await app.openTerminal(TheiaTerminal);
|
||||
if (process.platform === 'win32') {
|
||||
await terminal.submit('dir');
|
||||
} else {
|
||||
await terminal.submit('ls');
|
||||
}
|
||||
const contents = await terminal.contents();
|
||||
expect(contents).toContain('sample.txt');
|
||||
});
|
||||
|
||||
});
|
||||
190
examples/playwright/src/tests/theia-text-editor.test.ts
Normal file
190
examples/playwright/src/tests/theia-text-editor.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view';
|
||||
import { TheiaTextEditor } from '../theia-text-editor';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
test.describe('Theia Text Editor', () => {
|
||||
|
||||
let app: TheiaApp;
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
|
||||
// set auto-save preference to off
|
||||
const preferenceView = await app.openPreferences(TheiaPreferenceView);
|
||||
await preferenceView.setOptionsPreferenceById(PreferenceIds.Editor.AutoSave, DefaultPreferences.Editor.AutoSave.Off);
|
||||
await preferenceView.close();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
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.activate();
|
||||
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);
|
||||
const lineBelowSecond = await sampleTextEditor.textContentOfLineByLineNumber(3);
|
||||
await sampleTextEditor.deleteLineByLineNumber(2);
|
||||
expect(await sampleTextEditor.textContentOfLineByLineNumber(2)).toBe(lineBelowSecond);
|
||||
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);
|
||||
|
||||
expect(await sampleTextEditor.isTabVisible()).toBe(true);
|
||||
await sampleTextEditor.closeWithoutSave();
|
||||
expect(await sampleTextEditor.isTabVisible()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
69
examples/playwright/src/tests/theia-toolbar.test.ts
Normal file
69
examples/playwright/src/tests/theia-toolbar.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TheiaApp } from '../theia-app';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { TheiaToolbar } from '../theia-toolbar';
|
||||
|
||||
let app: TheiaApp;
|
||||
let toolbar: TheiaToolbar;
|
||||
|
||||
test.describe('Theia Toolbar', () => {
|
||||
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
app = await TheiaAppLoader.load({ playwright, browser });
|
||||
toolbar = new TheiaToolbar(app);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should toggle the toolbar and check visibility', async () => {
|
||||
// depending on the user settings we have different starting conditions for the toolbar
|
||||
const isShownInitially = await toolbar.isShown();
|
||||
expect(await toolbar.isShown()).toBe(isShownInitially);
|
||||
await toolbar.toggle();
|
||||
expect(await toolbar.isShown()).toBe(!isShownInitially);
|
||||
await toolbar.hide();
|
||||
expect(await toolbar.isShown()).toBe(false);
|
||||
await toolbar.show();
|
||||
expect(await toolbar.isShown()).toBe(true);
|
||||
});
|
||||
|
||||
test('should show the default toolbar tools of the sample Theia application', async () => {
|
||||
expect(await toolbar.toolbarItems()).toHaveLength(5);
|
||||
expect(await toolbar.toolbarItemIds()).toStrictEqual([
|
||||
'textEditor.commands.go.back',
|
||||
'textEditor.commands.go.forward',
|
||||
'workbench.action.splitEditorRight',
|
||||
'theia-sample-toolbar-contribution',
|
||||
'workbench.action.showCommands'
|
||||
]);
|
||||
});
|
||||
|
||||
test('should trigger the "Command Palette" toolbar tool as expect the command palette to open', async () => {
|
||||
const commandPaletteTool = await toolbar.toolBarItem('workbench.action.showCommands');
|
||||
expect(commandPaletteTool).toBeDefined;
|
||||
expect(await commandPaletteTool!.isEnabled()).toBe(true);
|
||||
|
||||
await commandPaletteTool!.trigger();
|
||||
expect(await app.quickCommandPalette.isOpen()).toBe(true);
|
||||
await app.quickCommandPalette.hide();
|
||||
expect(await app.quickCommandPalette.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
83
examples/playwright/src/tests/theia-workspace.test.ts
Normal file
83
examples/playwright/src/tests/theia-workspace.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TheiaAppLoader } from '../theia-app-loader';
|
||||
import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view';
|
||||
import { TheiaWorkspace } from '../theia-workspace';
|
||||
|
||||
test.describe('Theia Workspace', () => {
|
||||
let isElectron: boolean;
|
||||
test.beforeAll(async ({ playwright, browser }) => {
|
||||
isElectron = process.env.USE_ELECTRON === 'true';
|
||||
});
|
||||
|
||||
test('should be initialized empty by default', async ({ playwright, browser }) => {
|
||||
if (!isElectron) {
|
||||
const app = await TheiaAppLoader.load({ playwright, browser });
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER);
|
||||
expect(fileStatElements.length).toBe(0);
|
||||
await app.page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('should be initialized with the contents of a file location', async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
const app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
// resources/sample-files1 contains two folders and one file
|
||||
expect(await explorer.existsDirectoryNode('sampleFolder')).toBe(true);
|
||||
expect(await explorer.existsDirectoryNode('sampleFolderCompact')).toBe(true);
|
||||
expect(await explorer.existsFileNode('sample.txt')).toBe(true);
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('should be initialized with the contents of multiple file locations', async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([
|
||||
path.resolve(__dirname, '../../src/tests/resources/sample-files1'),
|
||||
path.resolve(__dirname, '../../src/tests/resources/sample-files2')]);
|
||||
const app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
const explorer = await app.openView(TheiaExplorerView);
|
||||
// resources/sample-files1 contains two folders and one file
|
||||
expect(await explorer.existsDirectoryNode('sampleFolder')).toBe(true);
|
||||
expect(await explorer.existsDirectoryNode('sampleFolderCompact')).toBe(true);
|
||||
expect(await explorer.existsFileNode('sample.txt')).toBe(true);
|
||||
// resources/sample-files2 contains one file
|
||||
expect(await explorer.existsFileNode('another-sample.txt')).toBe(true);
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
test('open sample.txt via file menu', async ({ playwright, browser }) => {
|
||||
const ws = new TheiaWorkspace([path.resolve(__dirname, '../../src/tests/resources/sample-files1')]);
|
||||
const app = await TheiaAppLoader.load({ playwright, browser }, ws);
|
||||
const menuEntry = app.isElectron ? 'Open File...' : 'Open...';
|
||||
|
||||
await (await app.menuBar.openMenu('File')).clickMenuItem(menuEntry);
|
||||
const fileDialog = await app.page.waitForSelector('div[class="dialogBlock"]');
|
||||
expect(await fileDialog.isVisible()).toBe(true);
|
||||
|
||||
const fileEntry = app.page.getByText('sample.txt');
|
||||
await fileEntry.click();
|
||||
await app.page.locator('#theia-dialog-shell').getByRole('button', { name: 'Open' }).click();
|
||||
|
||||
const span = await app.page.waitForSelector('span:has-text("content line 2")');
|
||||
expect(await span.isVisible()).toBe(true);
|
||||
await app.page.close();
|
||||
});
|
||||
|
||||
});
|
||||
26
examples/playwright/src/theia-about-dialog.ts
Normal file
26
examples/playwright/src/theia-about-dialog.ts
Normal file
@@ -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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TheiaDialog } from './theia-dialog';
|
||||
|
||||
export class TheiaAboutDialog extends TheiaDialog {
|
||||
|
||||
override async isVisible(): Promise<boolean> {
|
||||
const dialog = await this.page.$(`${this.blockSelector} .theia-aboutDialog`);
|
||||
return !!dialog && dialog.isVisible();
|
||||
}
|
||||
|
||||
}
|
||||
163
examples/playwright/src/theia-app-loader.ts
Normal file
163
examples/playwright/src/theia-app-loader.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 STMicroelectronics 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Page, PlaywrightWorkerArgs, _electron as electron } from '@playwright/test';
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaWorkspace } from './theia-workspace';
|
||||
|
||||
export interface TheiaAppFactory<T extends TheiaApp> {
|
||||
new(page: Page, initialWorkspace: TheiaWorkspace, isElectron?: boolean): T;
|
||||
}
|
||||
|
||||
// TODO this is just a sketch, we need a proper way to configure tests and pass this configuration to the `TheiaAppLoader`:
|
||||
export interface TheiaPlaywrightTestConfig {
|
||||
useElectron?: {
|
||||
/** Path to the Theia Electron app package (absolute or relative to this package). */
|
||||
electronAppPath?: string,
|
||||
/** Path to the folder containing the plugins to load (absolute or relative to this package). */
|
||||
pluginsPath?: string,
|
||||
// eslint-disable-next-line max-len
|
||||
/** Electron launch options as [specified by Playwright](https://github.com/microsoft/playwright/blob/396487fc4c19bf27554eac9beea9db135e96cfb4/packages/playwright-core/types/types.d.ts#L14182). */
|
||||
launchOptions?: object,
|
||||
}
|
||||
}
|
||||
|
||||
function theiaAppFactory<T extends TheiaApp>(factory?: TheiaAppFactory<T>): TheiaAppFactory<T> {
|
||||
return (factory ?? TheiaApp) as TheiaAppFactory<T>;
|
||||
}
|
||||
|
||||
function initializeWorkspace(initialWorkspace?: TheiaWorkspace): TheiaWorkspace {
|
||||
const workspace = initialWorkspace ? initialWorkspace : new TheiaWorkspace();
|
||||
workspace.initialize();
|
||||
return workspace;
|
||||
}
|
||||
|
||||
namespace TheiaBrowserAppLoader {
|
||||
|
||||
export async function load<T extends TheiaApp>(
|
||||
page: Page,
|
||||
initialWorkspace?: TheiaWorkspace,
|
||||
factory?: TheiaAppFactory<T>
|
||||
): Promise<T> {
|
||||
const workspace = initializeWorkspace(initialWorkspace);
|
||||
return createAndLoad<T>(page, workspace, factory);
|
||||
}
|
||||
|
||||
async function createAndLoad<T extends TheiaApp>(
|
||||
page: Page,
|
||||
workspace: TheiaWorkspace,
|
||||
factory?: TheiaAppFactory<T>
|
||||
): Promise<T> {
|
||||
const appFactory = theiaAppFactory<T>(factory);
|
||||
const app = new appFactory(page, workspace, false);
|
||||
await loadOrReload(app, '/#' + app.workspace.pathAsPathComponent);
|
||||
await app.waitForShellAndInitialized();
|
||||
return app;
|
||||
}
|
||||
|
||||
async function loadOrReload(app: TheiaApp, url: string): Promise<void> {
|
||||
if (app.page.url() === url) {
|
||||
await app.page.reload();
|
||||
} else {
|
||||
const wasLoadedAlready = await app.isShellVisible();
|
||||
await app.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 app.page.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TheiaElectronAppLoader {
|
||||
|
||||
export async function load<T extends TheiaApp>(
|
||||
args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs,
|
||||
initialWorkspace?: TheiaWorkspace,
|
||||
factory?: TheiaAppFactory<T>,
|
||||
): Promise<T> {
|
||||
const workspace = initializeWorkspace(initialWorkspace);
|
||||
const electronConfig = args.useElectron ?? {
|
||||
electronAppPath: '../electron',
|
||||
pluginsPath: '../../plugins'
|
||||
};
|
||||
if (electronConfig === undefined || electronConfig.launchOptions === undefined && electronConfig.electronAppPath === undefined) {
|
||||
throw Error('The Theia Playwright configuration must either specify `useElectron.electronAppPath` or `useElectron.launchOptions`');
|
||||
}
|
||||
const appPath = electronConfig.electronAppPath!;
|
||||
const pluginsPath = electronConfig.pluginsPath;
|
||||
const launchOptions = electronConfig.launchOptions ?? {
|
||||
additionalArgs: ['--no-sandbox', '--no-cluster'],
|
||||
electronAppPath: appPath,
|
||||
pluginsPath: pluginsPath
|
||||
};
|
||||
const playwrightOptions = toPlaywrightOptions(launchOptions, workspace);
|
||||
console.log(`Launching Electron with options: ${JSON.stringify(playwrightOptions)}`);
|
||||
const electronApp = await electron.launch(playwrightOptions);
|
||||
const page = await electronApp.firstWindow();
|
||||
|
||||
const appFactory = theiaAppFactory<T>(factory);
|
||||
const app = new appFactory(page, workspace, true);
|
||||
await app.waitForShellAndInitialized();
|
||||
return app;
|
||||
}
|
||||
|
||||
export function toPlaywrightOptions(
|
||||
electronLaunchOptions: { additionalArgs: string[], electronAppPath: string, pluginsPath?: string } | object,
|
||||
workspace?: TheiaWorkspace
|
||||
): {
|
||||
args: string[]
|
||||
} | object {
|
||||
if ('additionalArgs' in electronLaunchOptions && 'electronAppPath' in electronLaunchOptions) {
|
||||
const args = [
|
||||
electronLaunchOptions.electronAppPath,
|
||||
...electronLaunchOptions.additionalArgs,
|
||||
`--app-project-path=${electronLaunchOptions.electronAppPath}`
|
||||
];
|
||||
if (electronLaunchOptions.pluginsPath) {
|
||||
args.push(`--plugins=local-dir:${electronLaunchOptions.pluginsPath}`);
|
||||
}
|
||||
if (workspace) {
|
||||
args.push(workspace.path);
|
||||
}
|
||||
|
||||
return {
|
||||
args: args
|
||||
};
|
||||
}
|
||||
return electronLaunchOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TheiaAppLoader {
|
||||
|
||||
export async function load<T extends TheiaApp>(
|
||||
args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs,
|
||||
initialWorkspace?: TheiaWorkspace,
|
||||
factory?: TheiaAppFactory<T>,
|
||||
): Promise<T> {
|
||||
if (process.env.USE_ELECTRON === 'true') {
|
||||
// disable native elements and early window to avoid issues with the electron app
|
||||
process.env.THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS = '1';
|
||||
process.env.THEIA_ELECTRON_NO_EARLY_WINDOW = '1';
|
||||
process.env.THEIA_NO_SPLASH = 'true';
|
||||
return TheiaElectronAppLoader.load(args, initialWorkspace, factory);
|
||||
}
|
||||
const page = await args.browser.newPage();
|
||||
return TheiaBrowserAppLoader.load(page, initialWorkspace, factory);
|
||||
}
|
||||
}
|
||||
189
examples/playwright/src/theia-app.ts
Normal file
189
examples/playwright/src/theia-app.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021-2023 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-only 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 { TheiaTerminal } from './theia-terminal';
|
||||
import { TheiaView } from './theia-view';
|
||||
import { TheiaWorkspace } from './theia-workspace';
|
||||
|
||||
export interface TheiaAppData {
|
||||
loadingSelector: string;
|
||||
shellSelector: string;
|
||||
};
|
||||
|
||||
export const DefaultTheiaAppData: TheiaAppData = {
|
||||
loadingSelector: '.theia-preload',
|
||||
shellSelector: '.theia-ApplicationShell'
|
||||
};
|
||||
|
||||
export class TheiaApp {
|
||||
|
||||
statusBar: TheiaStatusBar;
|
||||
quickCommandPalette: TheiaQuickCommandPalette;
|
||||
menuBar: TheiaMenuBar;
|
||||
|
||||
protected appData = DefaultTheiaAppData;
|
||||
|
||||
public constructor(
|
||||
public page: Page,
|
||||
public workspace: TheiaWorkspace,
|
||||
public isElectron: boolean,
|
||||
) {
|
||||
this.statusBar = this.createStatusBar();
|
||||
this.quickCommandPalette = this.createQuickCommandPalette();
|
||||
this.menuBar = this.createMenuBar();
|
||||
}
|
||||
|
||||
protected createStatusBar(): TheiaStatusBar {
|
||||
return new TheiaStatusBar(this);
|
||||
}
|
||||
|
||||
protected createQuickCommandPalette(): TheiaQuickCommandPalette {
|
||||
return new TheiaQuickCommandPalette(this);
|
||||
}
|
||||
|
||||
protected createMenuBar(): TheiaMenuBar {
|
||||
return new TheiaMenuBar(this);
|
||||
}
|
||||
|
||||
async isShellVisible(): Promise<boolean> {
|
||||
return this.page.isVisible(this.appData.shellSelector);
|
||||
}
|
||||
|
||||
async waitForShellAndInitialized(): Promise<void> {
|
||||
await this.page.waitForSelector(this.appData.loadingSelector, { state: 'detached' });
|
||||
await this.page.waitForSelector(this.appData.shellSelector);
|
||||
await this.waitForInitialized();
|
||||
}
|
||||
|
||||
async isMainContentPanelVisible(): Promise<boolean> {
|
||||
const contentPanel = await this.page.$('#theia-main-content-panel');
|
||||
return !!contentPanel && contentPanel.isVisible();
|
||||
}
|
||||
|
||||
async openPreferences(viewFactory: { new(app: TheiaApp): TheiaPreferenceView }, preferenceScope = TheiaPreferenceScope.Workspace): Promise<TheiaPreferenceView> {
|
||||
const view = new viewFactory(this);
|
||||
if (await view.isTabVisible()) {
|
||||
await view.activate();
|
||||
return view;
|
||||
}
|
||||
await view.open(preferenceScope);
|
||||
return view;
|
||||
}
|
||||
|
||||
async openView<T extends TheiaView>(viewFactory: { new(app: TheiaApp): T }): Promise<T> {
|
||||
const view = new viewFactory(this);
|
||||
if (await view.isTabVisible()) {
|
||||
await view.activate();
|
||||
return view;
|
||||
}
|
||||
await view.open();
|
||||
return view;
|
||||
}
|
||||
|
||||
async openEditor<T extends TheiaEditor>(filePath: string,
|
||||
editorFactory: { new(fp: string, app: TheiaApp): T },
|
||||
editorName?: string, expectFileNodes = true): Promise<T> {
|
||||
const explorer = await this.openView(TheiaExplorerView);
|
||||
if (!explorer) {
|
||||
throw Error('TheiaExplorerView could not be opened.');
|
||||
}
|
||||
if (expectFileNodes) {
|
||||
await explorer.waitForVisibleFileNodes();
|
||||
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<T extends TheiaEditor>(filePath: string, editorFactory: { new(fp: string, app: TheiaApp): T }): Promise<T> {
|
||||
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;
|
||||
}
|
||||
|
||||
async openTerminal<T extends TheiaTerminal>(terminalFactory: { new(id: string, app: TheiaApp): T }): Promise<T> {
|
||||
const mainMenu = await this.menuBar.openMenu('Terminal');
|
||||
const menuItem = await mainMenu.menuItemByName('New Terminal');
|
||||
if (!menuItem) {
|
||||
throw Error('Menu item \'New Terminal\' could not be found.');
|
||||
}
|
||||
|
||||
const newTabIds = await this.runAndWaitForNewTabs(() => menuItem.click());
|
||||
if (newTabIds.length > 1) {
|
||||
console.warn('More than one new tab detected after opening the terminal');
|
||||
}
|
||||
|
||||
return new terminalFactory(newTabIds[0], this);
|
||||
}
|
||||
|
||||
protected async runAndWaitForNewTabs(command: () => Promise<void>): Promise<string[]> {
|
||||
const tabIdsBefore = await this.visibleTabIds();
|
||||
await command();
|
||||
return (await this.waitForNewTabs(tabIdsBefore)).filter(item => !tabIdsBefore.includes(item));
|
||||
}
|
||||
|
||||
protected async waitForNewTabs(tabIds: string[]): Promise<string[]> {
|
||||
let tabIdsCurrent: string[];
|
||||
while ((tabIdsCurrent = (await this.visibleTabIds())).length <= tabIds.length) {
|
||||
console.debug('Awaiting a new tab to appear');
|
||||
}
|
||||
return tabIdsCurrent;
|
||||
}
|
||||
|
||||
protected async visibleTabIds(): Promise<string[]> {
|
||||
const tabs = await this.page.$$('.lm-TabBar-tab');
|
||||
const tabIds = (await Promise.all(tabs.map(tab => tab.getAttribute('id')))).filter(id => !!id);
|
||||
return tabIds as string[];
|
||||
}
|
||||
|
||||
/** Specific Theia apps may add additional conditions to wait for. */
|
||||
async waitForInitialized(): Promise<void> {
|
||||
// empty by default
|
||||
}
|
||||
|
||||
}
|
||||
42
examples/playwright/src/theia-context-menu.ts
Normal file
42
examples/playwright/src/theia-context-menu.ts
Normal file
@@ -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-only 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<TheiaContextMenu> {
|
||||
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<ElementHandle<SVGElement | HTMLElement>>): Promise<TheiaContextMenu> {
|
||||
const elementHandle = await element();
|
||||
await elementHandle.click({ button: 'right' });
|
||||
return TheiaContextMenu.returnWhenVisible(app);
|
||||
}
|
||||
|
||||
private static async returnWhenVisible(app: TheiaApp): Promise<TheiaContextMenu> {
|
||||
const menu = new TheiaContextMenu(app);
|
||||
await menu.waitForVisible();
|
||||
return menu;
|
||||
}
|
||||
|
||||
}
|
||||
114
examples/playwright/src/theia-dialog.ts
Normal file
114
examples/playwright/src/theia-dialog.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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';
|
||||
protected errorSelector = this.blockSelector + ' .dialogContent';
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(`${this.blockSelector}`, { state: 'visible' });
|
||||
}
|
||||
|
||||
async waitForClosed(): Promise<void> {
|
||||
await this.page.waitForSelector(`${this.blockSelector}`, { state: 'detached' });
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
const pouDialogElement = await this.page.$(this.blockSelector);
|
||||
return pouDialogElement ? pouDialogElement.isVisible() : false;
|
||||
}
|
||||
|
||||
async title(): Promise<string | null> {
|
||||
const titleElement = await this.page.waitForSelector(`${this.titleSelector}`);
|
||||
return titleElement.textContent();
|
||||
}
|
||||
|
||||
async waitUntilTitleIsDisplayed(title: string): Promise<void> {
|
||||
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<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(this.contentSelector);
|
||||
}
|
||||
|
||||
protected async buttonElement(label: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(`${this.controlSelector} button:has-text("${label}")`);
|
||||
}
|
||||
|
||||
protected async buttonElementByClass(buttonClass: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(`${this.controlSelector} button${buttonClass}`);
|
||||
}
|
||||
|
||||
protected async validationElement(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(`${this.errorSelector} div.error`, { state: 'attached' });
|
||||
}
|
||||
|
||||
async getValidationText(): Promise<string | null> {
|
||||
const element = await this.validationElement();
|
||||
return element.textContent();
|
||||
}
|
||||
|
||||
async validationResult(): Promise<boolean> {
|
||||
const validationText = await this.getValidationText();
|
||||
return validationText !== '' ? false : true;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const closeButton = await this.page.waitForSelector(`${this.titleBarSelector} i.closeButton`);
|
||||
await closeButton.click();
|
||||
await this.waitForClosed();
|
||||
}
|
||||
|
||||
async clickButton(buttonLabel: string): Promise<void> {
|
||||
const buttonElement = await this.buttonElement(buttonLabel);
|
||||
await buttonElement.click();
|
||||
}
|
||||
|
||||
async isButtonDisabled(buttonLabel: string): Promise<boolean> {
|
||||
const buttonElement = await this.buttonElement(buttonLabel);
|
||||
return buttonElement.isDisabled();
|
||||
}
|
||||
|
||||
async clickMainButton(): Promise<void> {
|
||||
const buttonElement = await this.buttonElementByClass('.theia-button.main');
|
||||
await buttonElement.click();
|
||||
}
|
||||
|
||||
async clickSecondaryButton(): Promise<void> {
|
||||
const buttonElement = await this.buttonElementByClass('.theia-button.secondary');
|
||||
await buttonElement.click();
|
||||
}
|
||||
|
||||
async waitUntilMainButtonIsEnabled(): Promise<void> {
|
||||
await this.page.waitForFunction(predicate => {
|
||||
const button = document.querySelector<HTMLButtonElement>(predicate.buttonSelector);
|
||||
return !!button && !button.disabled;
|
||||
}, { buttonSelector: `${this.controlSelector} > button.theia-button.main` });
|
||||
}
|
||||
|
||||
}
|
||||
73
examples/playwright/src/theia-editor.ts
Normal file
73
examples/playwright/src/theia-editor.ts
Normal file
@@ -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-only 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<boolean> {
|
||||
return await this.isTabVisible() && containsClass(this.tabElement(), 'theia-mod-dirty');
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.save();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async undo(times = 1): Promise<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
311
examples/playwright/src/theia-explorer-view.ts
Normal file
311
examples/playwright/src/theia-explorer-view.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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, OSUtil } from './util';
|
||||
|
||||
const TheiaExplorerViewData = {
|
||||
tabSelector: '#shell-tab-explorer-view-container',
|
||||
viewSelector: '#explorer-view-container--files',
|
||||
viewName: 'Explorer'
|
||||
};
|
||||
|
||||
export class TheiaExplorerFileStatNode extends TheiaTreeNode {
|
||||
|
||||
constructor(protected override elementHandle: ElementHandle<SVGElement | HTMLElement>, protected explorerView: TheiaExplorerView) {
|
||||
super(elementHandle, explorerView.app);
|
||||
}
|
||||
|
||||
async absolutePath(): Promise<string | null> {
|
||||
return this.elementHandle.getAttribute('title');
|
||||
}
|
||||
|
||||
async isFile(): Promise<boolean> {
|
||||
return ! await this.isFolder();
|
||||
}
|
||||
|
||||
async isFolder(): Promise<boolean> {
|
||||
return elementContainsClass(this.elementHandle, 'theia-DirNode');
|
||||
}
|
||||
|
||||
async getMenuItemByNamePath(names: string[], nodeSegmentLabel?: string): Promise<TheiaMenuItem> {
|
||||
const contextMenu = nodeSegmentLabel ? await this.openContextMenuOnSegment(nodeSegmentLabel) : 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<boolean>;
|
||||
export const DOT_FILES_FILTER: TheiaExplorerFileStatNodePredicate = async node => {
|
||||
const label = await node.label();
|
||||
return label ? !label.startsWith('.') : true;
|
||||
};
|
||||
|
||||
export class TheiaExplorerView extends TheiaView {
|
||||
|
||||
constructor(app: TheiaApp) {
|
||||
super(TheiaExplorerViewData, app);
|
||||
}
|
||||
|
||||
override async activate(): Promise<void> {
|
||||
await super.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
await viewElement?.waitForSelector('.theia-TreeContainer');
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
await this.clickButton('navigator.refresh');
|
||||
}
|
||||
|
||||
async collapseAll(): Promise<void> {
|
||||
await this.clickButton('navigator.collapse.all');
|
||||
}
|
||||
|
||||
protected async clickButton(id: string): Promise<void> {
|
||||
await this.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
await viewElement?.hover();
|
||||
const button = await viewElement?.waitForSelector(`#${normalizeId(id)}`);
|
||||
await button?.click();
|
||||
}
|
||||
|
||||
async visibleFileStatNodes(filterPredicate: TheiaExplorerFileStatNodePredicate = (_ => Promise.resolve(true))): Promise<TheiaExplorerFileStatNode[]> {
|
||||
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, compact = false): Promise<TheiaExplorerFileStatNode> {
|
||||
const file = await this.fileStatNode(label, compact);
|
||||
if (!file) { throw Error('File stat node could not be retrieved by path fragments'); }
|
||||
return file;
|
||||
}
|
||||
|
||||
async fileStatNode(filePath: string, compact = false): Promise<TheiaExplorerFileStatNode | undefined> {
|
||||
return compact ? this.compactFileStatNode(filePath) : this.fileStatNodeBySegments(...filePath.split('/'));
|
||||
}
|
||||
|
||||
protected async fileStatNodeBySegments(...pathFragments: string[]): Promise<TheiaExplorerFileStatNode | undefined> {
|
||||
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;
|
||||
}
|
||||
|
||||
protected async compactFileStatNode(path: string): Promise<TheiaExplorerFileStatNode | undefined> {
|
||||
// default setting `explorer.compactFolders=true` renders folders in a compact form - single child folders will be compressed in a combined tree element
|
||||
await super.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
|
||||
// check if first segment folder needs to be expanded first (if folder has never been expanded, it will not show the compact folder structure)
|
||||
await this.waitForVisibleFileNodes();
|
||||
const firstSegment = path.split('/')[0];
|
||||
const selector = this.treeNodeSelector(firstSegment);
|
||||
const folderElement = await viewElement?.$(selector);
|
||||
if (folderElement && await folderElement.isVisible()) {
|
||||
const folderNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
|
||||
if (!folderNode) {
|
||||
throw new Error(`Tree node '${selector}' not found in explorer`);
|
||||
}
|
||||
const folderFileStatNode = new TheiaExplorerFileStatNode(folderNode, this);
|
||||
if (await folderFileStatNode.isCollapsed()) {
|
||||
await folderFileStatNode.expand();
|
||||
}
|
||||
}
|
||||
// now get tree node via the full path
|
||||
const fullPathSelector = this.treeNodeSelector(path);
|
||||
const treeNode = await viewElement?.waitForSelector(fullPathSelector, { state: 'visible' });
|
||||
if (!treeNode) {
|
||||
throw new Error(`Tree node '${fullPathSelector}' not found in explorer`);
|
||||
}
|
||||
return new TheiaExplorerFileStatNode(treeNode, this);
|
||||
}
|
||||
|
||||
async selectTreeNode(filePath: string): Promise<void> {
|
||||
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: [OSUtil.isMacOS ? 'Meta' : 'Control'] });
|
||||
// make sure the click has been acted-upon before returning
|
||||
while (!await this.isTreeNodeSelected(filePath)) {
|
||||
console.debug('Waiting for clicked tree node to be selected: ' + filePath);
|
||||
}
|
||||
}
|
||||
await this.page.waitForSelector(this.treeNodeSelector(filePath) + '.theia-mod-selected');
|
||||
}
|
||||
|
||||
async isTreeNodeSelected(filePath: string): Promise<boolean> {
|
||||
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.pathAsPathComponent;
|
||||
const nodeId = `${workspacePath}:${workspacePath}/${filePath}`;
|
||||
if (OSUtil.isWindows) {
|
||||
return nodeId.replace('\\', '/');
|
||||
}
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
async clickContextMenuItem(file: string, path: string[], nodeSegmentLabel?: string): Promise<void> {
|
||||
await this.activate();
|
||||
const fileStatNode = await this.fileStatNode(file, !!nodeSegmentLabel);
|
||||
if (!fileStatNode) { throw Error('File stat node could not be retrieved by path fragments'); }
|
||||
const menuItem = await fileStatNode.getMenuItemByNamePath(path, nodeSegmentLabel);
|
||||
await menuItem.click();
|
||||
}
|
||||
|
||||
protected async existsNode(path: string, isDirectory: boolean, compact = false): Promise<boolean> {
|
||||
const fileStatNode = await this.fileStatNode(path, compact);
|
||||
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<boolean> {
|
||||
return this.existsNode(path, false);
|
||||
}
|
||||
|
||||
async existsDirectoryNode(path: string, compact = false): Promise<boolean> {
|
||||
return this.existsNode(path, true, compact);
|
||||
}
|
||||
|
||||
async waitForTreeNodeVisible(path: string): Promise<void> {
|
||||
// wait for tree node to be visible, e.g. after triggering create
|
||||
const viewElement = await this.viewElement();
|
||||
await viewElement?.waitForSelector(this.treeNodeSelector(path), { state: 'visible' });
|
||||
}
|
||||
|
||||
async getNumberOfVisibleNodes(): Promise<number> {
|
||||
await this.activate();
|
||||
await this.refresh();
|
||||
const fileStatElements = await this.visibleFileStatNodes(DOT_FILES_FILTER);
|
||||
return fileStatElements.length;
|
||||
}
|
||||
|
||||
async deleteNode(path: string, confirm = true, nodeSegmentLabel?: string): Promise<void> {
|
||||
await this.activate();
|
||||
await this.clickContextMenuItem(path, ['Delete'], nodeSegmentLabel);
|
||||
|
||||
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, nodeSegmentLabel?: string): Promise<void> {
|
||||
await this.activate();
|
||||
await this.clickContextMenuItem(path, ['Rename'], nodeSegmentLabel);
|
||||
|
||||
const renameDialog = new TheiaRenameDialog(this.app);
|
||||
await renameDialog.waitForVisible();
|
||||
await renameDialog.enterNewName(newName);
|
||||
await renameDialog.waitUntilMainButtonIsEnabled();
|
||||
confirm ? await renameDialog.confirm() : await renameDialog.close();
|
||||
await renameDialog.waitForClosed();
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
override async waitForVisible(): Promise<void> {
|
||||
await super.waitForVisible();
|
||||
await this.page.waitForSelector(this.tabSelector, { state: 'visible' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until some non-dot file nodes are visible
|
||||
*/
|
||||
async waitForVisibleFileNodes(): Promise<void> {
|
||||
while ((await this.visibleFileStatNodes(DOT_FILES_FILTER)).length === 0) {
|
||||
console.debug('Awaiting for tree nodes to appear');
|
||||
}
|
||||
}
|
||||
|
||||
async waitForFileNodesToIncrease(numberBefore: number): Promise<void> {
|
||||
const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
|
||||
await this.page.waitForFunction(
|
||||
(predicate: { selector: string; numberBefore: number; }) => {
|
||||
const elements = document.querySelectorAll(predicate.selector);
|
||||
return !!elements && elements.length > predicate.numberBefore;
|
||||
},
|
||||
{ selector: fileStatNodesSelector, numberBefore }
|
||||
);
|
||||
}
|
||||
|
||||
async waitForFileNodesToDecrease(numberBefore: number): Promise<void> {
|
||||
const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
|
||||
await this.page.waitForFunction(
|
||||
(predicate: { selector: string; numberBefore: number; }) => {
|
||||
const elements = document.querySelectorAll(predicate.selector);
|
||||
return !!elements && elements.length < predicate.numberBefore;
|
||||
},
|
||||
{ selector: fileStatNodesSelector, numberBefore }
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
75
examples/playwright/src/theia-main-menu.ts
Normal file
75
examples/playwright/src/theia-main-menu.ts
Normal file
@@ -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-only 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 {
|
||||
override selector = '.lm-Menu.lm-MenuBar-menu';
|
||||
}
|
||||
|
||||
export class TheiaMenuBar extends TheiaPageObject {
|
||||
selector = normalizeId('#theia:menubar');
|
||||
|
||||
protected async menubarElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.selector);
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
const menuBar = await this.menubarElementHandle();
|
||||
return !!menuBar && menuBar.isVisible();
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async openMenu(menuName: string): Promise<TheiaMainMenu> {
|
||||
await this.waitForVisible();
|
||||
|
||||
const menuBarItem = await this.menuBarItem(menuName);
|
||||
if (!menuBarItem) {
|
||||
throw new Error(`Menu '${menuName}' not found!`);
|
||||
}
|
||||
|
||||
const mainMenu = new TheiaMainMenu(this.app);
|
||||
if (await mainMenu.isOpen()) {
|
||||
await menuBarItem.hover();
|
||||
} else {
|
||||
await menuBarItem.click();
|
||||
}
|
||||
await mainMenu.waitForVisible();
|
||||
return mainMenu;
|
||||
}
|
||||
|
||||
async visibleMenuBarItems(): Promise<string[]> {
|
||||
await this.waitForVisible();
|
||||
const items = await this.page.$$(this.menuBarItemSelector());
|
||||
return toTextContentArray(items);
|
||||
}
|
||||
|
||||
protected menuBarItem(label = ''): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.waitForSelector(this.menuBarItemSelector(label));
|
||||
}
|
||||
|
||||
protected menuBarItemSelector(label = ''): string {
|
||||
return `${this.selector} .lm-MenuBar-itemLabel >> text=${label}`;
|
||||
}
|
||||
|
||||
}
|
||||
75
examples/playwright/src/theia-menu-item.ts
Normal file
75
examples/playwright/src/theia-menu-item.ts
Normal file
@@ -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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
|
||||
import { elementContainsClass, textContent } from './util';
|
||||
|
||||
export class TheiaMenuItem {
|
||||
|
||||
constructor(protected element: ElementHandle<SVGElement | HTMLElement>) { }
|
||||
|
||||
protected labelElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.element.waitForSelector('.lm-Menu-itemLabel');
|
||||
}
|
||||
|
||||
protected shortCutElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.element.waitForSelector('.lm-Menu-itemShortcut');
|
||||
}
|
||||
|
||||
protected isHidden(): Promise<boolean> {
|
||||
return elementContainsClass(this.element, 'lm-mod-collapsed');
|
||||
}
|
||||
|
||||
async label(): Promise<string | undefined> {
|
||||
if (await this.isHidden()) {
|
||||
return undefined;
|
||||
}
|
||||
return textContent(this.labelElementHandle());
|
||||
}
|
||||
|
||||
async shortCut(): Promise<string | undefined> {
|
||||
if (await this.isHidden()) {
|
||||
return undefined;
|
||||
}
|
||||
return textContent(this.shortCutElementHandle());
|
||||
}
|
||||
|
||||
async hasSubmenu(): Promise<boolean> {
|
||||
if (await this.isHidden()) {
|
||||
return false;
|
||||
}
|
||||
return (await this.element.getAttribute('data-type')) === 'submenu';
|
||||
}
|
||||
|
||||
async isEnabled(): Promise<boolean> {
|
||||
const classAttribute = (await this.element.getAttribute('class'));
|
||||
if (classAttribute === undefined || classAttribute === null) {
|
||||
return false;
|
||||
}
|
||||
return !classAttribute.includes('lm-mod-disabled') && !classAttribute.includes('lm-mod-collapsed');
|
||||
}
|
||||
|
||||
async click(): Promise<void> {
|
||||
return this.element.waitForSelector('.lm-Menu-itemLabel')
|
||||
.then(labelElement => labelElement.click({ position: { x: 10, y: 10 } }));
|
||||
}
|
||||
|
||||
async hover(): Promise<void> {
|
||||
return this.element.hover();
|
||||
}
|
||||
|
||||
}
|
||||
116
examples/playwright/src/theia-menu.ts
Normal file
116
examples/playwright/src/theia-menu.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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 = '.lm-Menu';
|
||||
|
||||
protected async menuElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.selector);
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async isOpen(): Promise<boolean> {
|
||||
const menu = await this.menuElementHandle();
|
||||
return !!menu && menu.isVisible();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (!await this.isOpen()) {
|
||||
return;
|
||||
}
|
||||
await this.page.mouse.click(0, 0);
|
||||
await this.page.waitForSelector(this.selector, { state: 'detached' });
|
||||
}
|
||||
|
||||
async menuItems(): Promise<TheiaMenuItem[]> {
|
||||
if (!await this.isOpen()) {
|
||||
throw new Error('Menu must be open before accessing menu items');
|
||||
}
|
||||
const menuHandle = await this.menuElementHandle();
|
||||
if (!menuHandle) {
|
||||
return [];
|
||||
}
|
||||
const items = await menuHandle.$$('.lm-Menu-content .lm-Menu-item');
|
||||
return items.map(element => new TheiaMenuItem(element));
|
||||
}
|
||||
|
||||
async clickMenuItem(name: string): Promise<void> {
|
||||
if (!await this.isOpen()) {
|
||||
throw new Error('Menu must be open before clicking menu items');
|
||||
}
|
||||
return (await this.page.waitForSelector(this.menuItemSelector(name))).click();
|
||||
}
|
||||
|
||||
async menuItemByName(name: string): Promise<TheiaMenuItem | undefined> {
|
||||
if (!await this.isOpen()) {
|
||||
throw new Error('Menu must be open before accessing menu items by name');
|
||||
}
|
||||
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<TheiaMenuItem | undefined> {
|
||||
if (!await this.isOpen()) {
|
||||
throw new Error('Menu must be open before accessing menu items by path');
|
||||
}
|
||||
|
||||
let item;
|
||||
for (let index = 0; index < names.length; index++) {
|
||||
item = await this.page.waitForSelector(this.menuItemSelector(names[index]), { state: 'visible' });
|
||||
// For all items except the last one, hover to open submenu
|
||||
if (index < names.length - 1) {
|
||||
await item.scrollIntoViewIfNeeded();
|
||||
await item.hover();
|
||||
}
|
||||
}
|
||||
|
||||
const menuItemHandle = await item?.$('xpath=..');
|
||||
if (menuItemHandle) {
|
||||
return new TheiaMenuItem(menuItemHandle);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected menuItemSelector(label = ''): string {
|
||||
return `.lm-Menu-content .lm-Menu-itemLabel >> text=${label}`;
|
||||
}
|
||||
|
||||
async visibleMenuItems(): Promise<string[]> {
|
||||
if (!await this.isOpen()) {
|
||||
return [];
|
||||
}
|
||||
const menuItems = await this.menuItems();
|
||||
const labels = await Promise.all(menuItems.map(item => item.label()));
|
||||
return labels.filter(isDefined);
|
||||
}
|
||||
|
||||
}
|
||||
183
examples/playwright/src/theia-monaco-editor.ts
Normal file
183
examples/playwright/src/theia-monaco-editor.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle, Locator } from '@playwright/test';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
import { TheiaApp } from './theia-app';
|
||||
|
||||
/**
|
||||
* Monaco editor page object.
|
||||
*
|
||||
* Note: The constructor overload using `selector: string` is deprecated. Use the `locator: Locator` overload instead.
|
||||
*
|
||||
*/
|
||||
export class TheiaMonacoEditor extends TheiaPageObject {
|
||||
|
||||
public readonly locator: Locator;
|
||||
|
||||
protected readonly LINES_SELECTOR = '.view-lines > .view-line';
|
||||
|
||||
/**
|
||||
* Monaco editor page object.
|
||||
*
|
||||
* @param locator The locator of the editor.
|
||||
* @param app The Theia app instance.
|
||||
*/
|
||||
constructor(locator: Locator, app: TheiaApp);
|
||||
|
||||
/**
|
||||
* @deprecated Use the `constructor(locator: Locator, app: TheiaApp)` overload instead.
|
||||
*/
|
||||
constructor(selector: string, app: TheiaApp);
|
||||
|
||||
constructor(locatorOrString: Locator | string, app: TheiaApp) {
|
||||
super(app);
|
||||
if (typeof locatorOrString === 'string') {
|
||||
this.locator = app.page.locator(locatorOrString);
|
||||
} else {
|
||||
this.locator = locatorOrString;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
// wait until lines are created
|
||||
await this.locator.evaluate(editor =>
|
||||
editor.querySelectorAll(this.LINES_SELECTOR).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `locator` instead. To get the element handle use `await locator.elementHandle()`.
|
||||
* @returns The view element of the editor.
|
||||
*/
|
||||
protected async viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.locator.elementHandle();
|
||||
}
|
||||
|
||||
async numberOfLines(): Promise<number> {
|
||||
await this.waitForVisible();
|
||||
const lineElements = await this.locator.locator(this.LINES_SELECTOR).all();
|
||||
return lineElements.length;
|
||||
}
|
||||
|
||||
async textContentOfLineByLineNumber(lineNumber: number): Promise<string | undefined> {
|
||||
await this.waitForVisible();
|
||||
const lineElement = await this.line(lineNumber);
|
||||
const content = await lineElement?.textContent();
|
||||
return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `line(lineNumber: number)` instead.
|
||||
* @param lineNumber The line number to retrieve.
|
||||
* @returns The line element of the editor.
|
||||
*/
|
||||
async lineByLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
const lineLocator = await this.line(lineNumber);
|
||||
return (await lineLocator.elementHandle()) ?? undefined;
|
||||
}
|
||||
|
||||
async line(lineNumber: number): Promise<Locator> {
|
||||
await this.waitForVisible();
|
||||
const lines = await this.locator.locator(this.LINES_SELECTOR).all();
|
||||
if (!lines || lines.length === 0) {
|
||||
throw new Error('Couldn\'t retrieve lines of monaco editor');
|
||||
}
|
||||
|
||||
const linesWithXCoordinates = [];
|
||||
for (const line of lines) {
|
||||
await line.waitFor({ state: 'visible' });
|
||||
const box = await line.boundingBox();
|
||||
linesWithXCoordinates.push({ x: box ? box.x : Number.MAX_VALUE, line });
|
||||
}
|
||||
linesWithXCoordinates.sort((a, b) => a.x.toString().localeCompare(b.x.toString()));
|
||||
const lineInfo = linesWithXCoordinates[lineNumber - 1];
|
||||
if (!lineInfo) {
|
||||
throw new Error(`Could not find line number ${lineNumber}`);
|
||||
}
|
||||
return lineInfo.line;
|
||||
}
|
||||
|
||||
async textContentOfLineContainingText(text: string): Promise<string | undefined> {
|
||||
await this.waitForVisible();
|
||||
const lineElement = await this.lineWithText(text);
|
||||
const content = await lineElement?.textContent();
|
||||
return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `lineWithText(text: string)` instead.
|
||||
* @param text The text to search for in the editor.
|
||||
* @returns The line element containing the text.
|
||||
*/
|
||||
async lineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
const lineWithText = await this.lineWithText(text);
|
||||
return await lineWithText?.elementHandle() ?? undefined;
|
||||
}
|
||||
|
||||
async lineWithText(text: string): Promise<Locator | undefined> {
|
||||
const lineWithText = this.locator.locator(`${this.LINES_SELECTOR}:has-text("${text}")`);
|
||||
await lineWithText.waitFor({ state: 'visible' });
|
||||
return lineWithText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The text content of the editor.
|
||||
*/
|
||||
async editorText(): Promise<string | undefined> {
|
||||
const lines: string[] = [];
|
||||
const linesCount = await this.numberOfLines();
|
||||
if (linesCount === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
for (let line = 1; line <= linesCount; line++) {
|
||||
const lineText = await this.textContentOfLineByLineNumber(line);
|
||||
if (lineText === undefined) {
|
||||
break;
|
||||
}
|
||||
lines.push(lineText);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text to the editor.
|
||||
* @param text The text to add to the editor.
|
||||
* @param lineNumber The line number where to add the text. Default is 1.
|
||||
*/
|
||||
async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
|
||||
const line = await this.line(lineNumber);
|
||||
await line?.click();
|
||||
await this.page.keyboard.type(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if the editor is focused, `false` otherwise.
|
||||
*/
|
||||
async isFocused(): Promise<boolean> {
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
const editorClass = await this.locator.getAttribute('class');
|
||||
return editorClass?.includes('focused') ?? false;
|
||||
}
|
||||
|
||||
protected replaceEditorSymbolsWithSpace(content: string): string | Promise<string | undefined> {
|
||||
// [ ] => \u00a0 -- NO-BREAK SPACE
|
||||
// [·] · => \u00b7 -- MIDDLE DOT
|
||||
// [] ‌ => \u200c -- ZERO WIDTH NON-JOINER
|
||||
return content.replace(/[\u00a0\u00b7]/g, ' ').replace(/[\u200c]/g, '');
|
||||
}
|
||||
}
|
||||
254
examples/playwright/src/theia-notebook-cell.ts
Normal file
254
examples/playwright/src/theia-notebook-cell.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { expect, FrameLocator, Locator } from '@playwright/test';
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaMonacoEditor } from './theia-monaco-editor';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
|
||||
export type CellStatus = 'success' | 'error' | 'waiting';
|
||||
|
||||
/**
|
||||
* Page object for a Theia notebook cell.
|
||||
*/
|
||||
export class TheiaNotebookCell extends TheiaPageObject {
|
||||
|
||||
protected cellEditor: TheiaNotebookCellEditor;
|
||||
|
||||
constructor(readonly locator: Locator, protected readonly notebookEditorLocator: Locator, app: TheiaApp) {
|
||||
super(app);
|
||||
const editorLocator = locator.locator('div.theia-notebook-cell-editor');
|
||||
this.cellEditor = new TheiaNotebookCellEditor(editorLocator, app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The cell editor page object.
|
||||
*/
|
||||
get editor(): TheiaNotebookCellEditor {
|
||||
return this.cellEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Locator for the sidebar (left) of the cell.
|
||||
*/
|
||||
sidebar(): Locator {
|
||||
return this.locator.locator('div.theia-notebook-cell-sidebar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Locator for the toolbar (top) of the cell.
|
||||
*/
|
||||
toolbar(): Locator {
|
||||
return this.locator.locator('div.theia-notebook-cell-toolbar');
|
||||
}
|
||||
/**
|
||||
* @returns Locator for the statusbar (bottom) of the cell.
|
||||
*/
|
||||
statusbar(): Locator {
|
||||
return this.locator.locator('div.notebook-cell-status');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Locator for the status icon inside the statusbar of the cell.
|
||||
*/
|
||||
statusIcon(): Locator {
|
||||
return this.statusbar().locator('span.notebook-cell-status-item');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` id the cell is a code cell, `false` otherwise.
|
||||
*/
|
||||
async isCodeCell(): Promise<boolean> {
|
||||
const classAttribute = await this.mode();
|
||||
return classAttribute !== 'markdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The mode of the cell, e.g. 'python', 'markdown', etc.
|
||||
*/
|
||||
async mode(): Promise<string> {
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
const editorElement = await this.editor.locator.elementHandle();
|
||||
if (editorElement === null) {
|
||||
throw new Error('Could not find editor element for the notebook cell.');
|
||||
}
|
||||
const classAttribute = await editorElement.getAttribute('data-mode-id');
|
||||
if (classAttribute === null) {
|
||||
throw new Error('Could not find mode attribute for the notebook cell.');
|
||||
}
|
||||
return classAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The text content of the cell editor.
|
||||
*/
|
||||
async editorText(): Promise<string | undefined> {
|
||||
return this.editor.monacoEditor.editorText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text to the editor of the cell.
|
||||
* @param text The text to add to the editor.
|
||||
* @param lineNumber The line number where to add the text. Default is 1.
|
||||
*/
|
||||
async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
|
||||
await this.editor.monacoEditor.addEditorText(text, lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param wait If `true` waits for the cell to finish execution, otherwise returns immediately.
|
||||
*/
|
||||
async execute(wait = true): Promise<void> {
|
||||
const execButton = this.sidebar().locator('[id="notebook.cell.execute-cell"]');
|
||||
await execButton.waitFor({ state: 'visible' });
|
||||
await execButton.click();
|
||||
if (wait) {
|
||||
// wait for the cell to finish execution
|
||||
await this.waitForCellToFinish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the cell into two cells by dividing the cell text on current cursor position.
|
||||
*/
|
||||
async splitCell(): Promise<void> {
|
||||
const execButton = this.toolbar().locator('[id="notebook.cell.split"]');
|
||||
await execButton.waitFor({ state: 'visible' });
|
||||
await execButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cell.
|
||||
*/
|
||||
async deleteCell(): Promise<void> {
|
||||
const button = this.toolbar().locator('[id="notebook.cell.delete"]');
|
||||
await button.waitFor({ state: 'visible' });
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the cell to reach success or error status.
|
||||
*/
|
||||
async waitForCellToFinish(): Promise<void> {
|
||||
await expect(this.statusIcon()).toHaveClass(/(.*codicon-check.*|.*codicon-error.*)/);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The status of the cell. Possible values are 'success', 'error', 'waiting'.
|
||||
*/
|
||||
async status(): Promise<CellStatus> {
|
||||
const statusLocator = this.statusIcon();
|
||||
const status = this.toCellStatus(await (await statusLocator.elementHandle())?.getAttribute('class') ?? '');
|
||||
return status;
|
||||
}
|
||||
|
||||
protected toCellStatus(classes: string): CellStatus {
|
||||
return classes.includes('codicon-check') ? 'success'
|
||||
: classes.includes('codicon-error') ? 'error'
|
||||
: 'waiting';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param acceptEmpty If `true`, accepts empty execution count. Otherwise waits for the execution count to be set.
|
||||
* @returns The execution count of the cell.
|
||||
*/
|
||||
async executionCount(acceptEmpty: boolean = false): Promise<string | undefined> {
|
||||
const countNode = this.sidebar().locator('span.theia-notebook-code-cell-execution-order');
|
||||
await countNode.waitFor({ state: 'visible' });
|
||||
await this.waitForCellToFinish();
|
||||
// Wait for the execution count to be set.
|
||||
await countNode.page().waitForFunction(
|
||||
arg => {
|
||||
const text = arg.ele?.textContent;
|
||||
return text && (arg.acceptEmpty || text !== '[ ]');
|
||||
},
|
||||
{ ele: await countNode.elementHandle(), acceptEmpty },
|
||||
);
|
||||
const counterText = await countNode.textContent();
|
||||
return counterText?.substring(1, counterText.length - 1); // remove square brackets
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if the cell is selected (blue vertical line), `false` otherwise.
|
||||
*/
|
||||
async isSelected(): Promise<boolean> {
|
||||
const markerClass = await this.locator.locator('div.theia-notebook-cell-marker').getAttribute('class');
|
||||
return markerClass?.includes('theia-notebook-cell-marker-selected') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The output text of the cell.
|
||||
*/
|
||||
async outputText(): Promise<string> {
|
||||
const outputContainer = await this.outputContainer();
|
||||
await outputContainer.waitFor({ state: 'visible' });
|
||||
// By default just collect all spans text.
|
||||
const spansLocator: Locator = outputContainer.locator('span:not(:has(*))'); // ignore nested spans
|
||||
const spanTexts = await spansLocator.evaluateAll(spans => spans.map(span => span.textContent?.trim())
|
||||
.filter(text => text !== undefined && text.length > 0));
|
||||
return spanTexts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the cell itself not it's editor. Important for shortcut usage like copy-, cut-, paste-cell.
|
||||
*/
|
||||
async selectCell(): Promise<void> {
|
||||
await this.sidebar().click();
|
||||
}
|
||||
|
||||
async outputContainer(): Promise<Locator> {
|
||||
const outFrame = await this.outputFrame();
|
||||
// each cell has it's own output div with a unique id = cellHandle<handle>
|
||||
const cellOutput = outFrame.locator(`div#cellHandle${await this.cellHandle()}`);
|
||||
return cellOutput.locator('div.output-container');
|
||||
}
|
||||
|
||||
protected async cellHandle(): Promise<string | null> {
|
||||
const handle = await this.locator.getAttribute('data-cell-handle');
|
||||
if (handle === null) {
|
||||
throw new Error('Could not find cell handle attribute `data-cell-handle` for the notebook cell.');
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
protected async outputFrame(): Promise<FrameLocator> {
|
||||
const containerDiv = this.notebookEditorLocator.locator('div.theia-notebook-cell-output-webview');
|
||||
const webViewFrame = containerDiv.frameLocator('iframe.webview');
|
||||
await webViewFrame.locator('iframe').waitFor({ state: 'attached' });
|
||||
return webViewFrame.frameLocator('iframe');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the monaco editor inside a notebook cell.
|
||||
*/
|
||||
export class TheiaNotebookCellEditor extends TheiaPageObject {
|
||||
|
||||
public readonly monacoEditor: TheiaMonacoEditor;
|
||||
|
||||
constructor(readonly locator: Locator, app: TheiaApp) {
|
||||
super(app);
|
||||
this.monacoEditor = new TheiaMonacoEditor(locator.locator('.monaco-editor'), app);
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
return this.locator.isVisible();
|
||||
}
|
||||
}
|
||||
171
examples/playwright/src/theia-notebook-editor.ts
Normal file
171
examples/playwright/src/theia-notebook-editor.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Locator } from '@playwright/test';
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaEditor } from './theia-editor';
|
||||
import { TheiaNotebookCell } from './theia-notebook-cell';
|
||||
import { TheiaNotebookToolbar } from './theia-notebook-toolbar';
|
||||
import { TheiaQuickCommandPalette } from './theia-quick-command-palette';
|
||||
import { TheiaToolbarItem } from './theia-toolbar-item';
|
||||
import { normalizeId } from './util';
|
||||
|
||||
export namespace NotebookCommands {
|
||||
export const SELECT_KERNEL_COMMAND = 'notebook.selectKernel';
|
||||
export const ADD_NEW_CELL_COMMAND = 'notebook.add-new-code-cell';
|
||||
export const ADD_NEW_MARKDOWN_CELL_COMMAND = 'notebook.add-new-markdown-cell';
|
||||
export const EXECUTE_NOTEBOOK_COMMAND = 'notebook.execute';
|
||||
export const CLEAR_ALL_OUTPUTS_COMMAND = 'notebook.clear-all-outputs';
|
||||
export const EXPORT_COMMAND = 'jupyter.notebookeditor.export';
|
||||
}
|
||||
|
||||
export class TheiaNotebookEditor extends TheiaEditor {
|
||||
|
||||
constructor(filePath: string, app: TheiaApp) {
|
||||
// shell-tab-notebook::file://<path>
|
||||
// notebook:file://<path>
|
||||
super({
|
||||
tabSelector: normalizeId(`#shell-tab-notebook:${app.workspace.pathAsUrl(filePath)}`),
|
||||
viewSelector: normalizeId(`#notebook:${app.workspace.pathAsUrl(filePath)}`)
|
||||
}, app);
|
||||
}
|
||||
|
||||
protected viewLocator(): Locator {
|
||||
return this.page.locator(this.data.viewSelector);
|
||||
}
|
||||
|
||||
tabLocator(): Locator {
|
||||
return this.page.locator(this.data.tabSelector);
|
||||
}
|
||||
|
||||
override async waitForVisible(): Promise<void> {
|
||||
await super.waitForVisible();
|
||||
// wait for toolbar being rendered as it takes some time to load the kernel data.
|
||||
await this.notebookToolbar().waitForVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The main toolbar of the notebook editor.
|
||||
*/
|
||||
notebookToolbar(): TheiaNotebookToolbar {
|
||||
return new TheiaNotebookToolbar(this.viewLocator(), this.app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The name of the selected kernel.
|
||||
*/
|
||||
async selectedKernel(): Promise<string | undefined | null> {
|
||||
const kernelItem = await this.toolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
|
||||
if (!kernelItem) {
|
||||
throw new Error('Select kernel toolbar item not found.');
|
||||
}
|
||||
return this.notebookToolbar().locator.locator('#kernel-text').innerText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select a kernel using toolbar item.
|
||||
* @param kernelName The name of the kernel to select.
|
||||
*/
|
||||
async selectKernel(kernelName: string): Promise<void> {
|
||||
await this.triggerToolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
|
||||
const qInput = new TheiaQuickCommandPalette(this.app);
|
||||
const widget = await this.page.waitForSelector(qInput.selector, { timeout: 5000 });
|
||||
if (widget && !await qInput.isOpen()) {
|
||||
throw new Error('Failed to trigger kernel selection');
|
||||
}
|
||||
await qInput.type(kernelName, true);
|
||||
await qInput.hide();
|
||||
}
|
||||
|
||||
async availableKernels(): Promise<string[]> {
|
||||
await this.triggerToolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
|
||||
const qInput = new TheiaQuickCommandPalette(this.app);
|
||||
const widget = await this.page.waitForSelector(qInput.selector, { timeout: 5000 });
|
||||
if (widget && !await qInput.isOpen()) {
|
||||
throw new Error('Failed to trigger kernel selection');
|
||||
}
|
||||
await qInput.type('Python', false);
|
||||
try {
|
||||
const listItems = await Promise.all((await qInput.visibleItems()).map(async item => item.textContent()));
|
||||
await this.page.keyboard.press('Enter');
|
||||
await qInput.hide();
|
||||
return listItems.filter(item => item !== null) as string[];
|
||||
} finally {
|
||||
await qInput.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new code cell to the notebook.
|
||||
*/
|
||||
async addCodeCell(): Promise<void> {
|
||||
const currentCellsCount = (await this.cells()).length;
|
||||
// FIXME Command sometimes produces bogus Editor cell without the monaco editor.
|
||||
await this.triggerToolbarItem(NotebookCommands.ADD_NEW_CELL_COMMAND);
|
||||
await this.waitForCellCountChanged(currentCellsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new markdown cell to the notebook.
|
||||
*/
|
||||
async addMarkdownCell(): Promise<void> {
|
||||
const currentCellsCount = (await this.cells()).length;
|
||||
await this.triggerToolbarItem(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND);
|
||||
await this.waitForCellCountChanged(currentCellsCount);
|
||||
}
|
||||
|
||||
async waitForCellCountChanged(prevCount: number): Promise<void> {
|
||||
await this.viewLocator().locator('li.theia-notebook-cell').evaluateAll(
|
||||
(elements, currentCount) => elements.length !== currentCount, prevCount
|
||||
);
|
||||
}
|
||||
|
||||
async executeAllCells(): Promise<void> {
|
||||
await this.triggerToolbarItem(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND);
|
||||
}
|
||||
|
||||
async clearAllOutputs(): Promise<void> {
|
||||
await this.triggerToolbarItem(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND);
|
||||
}
|
||||
|
||||
async exportAs(): Promise<void> {
|
||||
await this.triggerToolbarItem(NotebookCommands.EXPORT_COMMAND);
|
||||
}
|
||||
|
||||
async cells(): Promise<TheiaNotebookCell[]> {
|
||||
const cellsLocator = this.viewLocator().locator('li.theia-notebook-cell');
|
||||
const cells: Array<TheiaNotebookCell> = [];
|
||||
for (const cellLocator of await cellsLocator.all()) {
|
||||
await cellLocator.waitFor({ state: 'visible' });
|
||||
cells.push(new TheiaNotebookCell(cellLocator, this.viewLocator(), this.app));
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
protected async triggerToolbarItem(id: string): Promise<void> {
|
||||
const item = await this.toolbarItem(id);
|
||||
if (!item) {
|
||||
throw new Error(`Toolbar item with id ${id} not found`);
|
||||
}
|
||||
await item.trigger();
|
||||
}
|
||||
|
||||
protected async toolbarItem(id: string): Promise<TheiaToolbarItem | undefined> {
|
||||
const toolBar = this.notebookToolbar();
|
||||
await toolBar.waitForVisible();
|
||||
return toolBar.toolBarItem(id);
|
||||
}
|
||||
}
|
||||
53
examples/playwright/src/theia-notebook-toolbar.ts
Normal file
53
examples/playwright/src/theia-notebook-toolbar.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle, Locator } from '@playwright/test';
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaToolbar } from './theia-toolbar';
|
||||
|
||||
export class TheiaNotebookToolbar extends TheiaToolbar {
|
||||
public readonly locator: Locator;
|
||||
|
||||
constructor(parentLocator: Locator, app: TheiaApp) {
|
||||
super(app);
|
||||
this.selector = 'div#notebook-main-toolbar';
|
||||
this.locator = parentLocator.locator(this.selector);
|
||||
}
|
||||
|
||||
protected override toolBarItemSelector(toolbarItemId = ''): string {
|
||||
return `div.theia-notebook-main-toolbar-item${toolbarItemId ? `[id="${toolbarItemId}"]` : ''}`;
|
||||
}
|
||||
|
||||
protected override async toolbarElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
// Use locator instead of page to find the toolbar element.
|
||||
return this.locator.elementHandle();
|
||||
}
|
||||
|
||||
override async waitForVisible(): Promise<void> {
|
||||
// Use locator instead of page to find the toolbar element.
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
override async waitUntilHidden(): Promise<void> {
|
||||
// Use locator instead of page to find the toolbar element.
|
||||
await this.locator.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
override async waitUntilShown(): Promise<void> {
|
||||
// Use locator instead of page to find the toolbar element.
|
||||
await this.locator.waitFor({ state: 'visible' });
|
||||
}
|
||||
}
|
||||
44
examples/playwright/src/theia-notification-indicator.ts
Normal file
44
examples/playwright/src/theia-notification-indicator.ts
Normal file
@@ -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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TheiaStatusIndicator } from './theia-status-indicator';
|
||||
|
||||
const NOTIFICATION_DOT_ICON = 'codicon-bell-dot';
|
||||
|
||||
export class TheiaNotificationIndicator extends TheiaStatusIndicator {
|
||||
id = 'theia-notification-center';
|
||||
|
||||
async hasNotifications(): Promise<boolean> {
|
||||
const container = await this.getElementHandle();
|
||||
const bellWithDot = await container.$(`.${NOTIFICATION_DOT_ICON}`);
|
||||
return Boolean(bellWithDot?.isVisible());
|
||||
}
|
||||
|
||||
override async waitForVisible(expectNotifications = false): Promise<void> {
|
||||
await super.waitForVisible();
|
||||
if (expectNotifications && !(await this.hasNotifications())) {
|
||||
throw new Error('No notifications when notifications expected.');
|
||||
}
|
||||
}
|
||||
|
||||
async toggleOverlay(): Promise<void> {
|
||||
const element = await this.getElementHandle();
|
||||
if (element) {
|
||||
await element.click();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
94
examples/playwright/src/theia-notification-overlay.ts
Normal file
94
examples/playwright/src/theia-notification-overlay.ts
Normal file
@@ -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-only 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(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<boolean> {
|
||||
const element = await this.page.$(`${this.containerSelector}.open`);
|
||||
return element ? element.isVisible() : false;
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(`${this.containerSelector}.open`);
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
if (!await this.isVisible()) {
|
||||
await this.notificationIndicator.toggleOverlay();
|
||||
}
|
||||
await this.waitForVisible();
|
||||
}
|
||||
|
||||
async toggle(): Promise<void> {
|
||||
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<void> {
|
||||
await this.activate();
|
||||
await this.page.waitForSelector(this.entrySelector(entryText));
|
||||
}
|
||||
|
||||
async waitForEntryDetached(entryText: string): Promise<void> {
|
||||
await this.activate();
|
||||
await this.page.waitForSelector(this.entrySelector(entryText), { state: 'detached' });
|
||||
}
|
||||
|
||||
async isEntryVisible(entryText: string): Promise<boolean> {
|
||||
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<void> {
|
||||
await this.activate();
|
||||
const element = await this.page.waitForSelector(this.clearAllButtonSelector);
|
||||
await element.click();
|
||||
await this.notificationIndicator.waitForVisible(false /* expectNotifications */);
|
||||
}
|
||||
|
||||
}
|
||||
88
examples/playwright/src/theia-output-channel.ts
Normal file
88
examples/playwright/src/theia-output-channel.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaOutputView } from './theia-output-view';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
import { isElementVisible } from './util';
|
||||
import { TheiaMonacoEditor } from './theia-monaco-editor';
|
||||
|
||||
export interface TheiaOutputViewChannelData {
|
||||
viewSelector: string;
|
||||
dataUri: string;
|
||||
channelName: string;
|
||||
}
|
||||
|
||||
export class TheiaOutputViewChannel extends TheiaPageObject {
|
||||
|
||||
protected monacoEditor: TheiaMonacoEditor;
|
||||
|
||||
constructor(protected readonly data: TheiaOutputViewChannelData, protected readonly outputView: TheiaOutputView) {
|
||||
super(outputView.app);
|
||||
this.monacoEditor = new TheiaMonacoEditor(this.page.locator(this.viewSelector), outputView.app);
|
||||
}
|
||||
|
||||
protected get viewSelector(): string {
|
||||
return this.data.viewSelector;
|
||||
}
|
||||
|
||||
protected get dataUri(): string | undefined {
|
||||
return this.data.dataUri;
|
||||
}
|
||||
|
||||
protected get channelName(): string | undefined {
|
||||
return this.data.channelName;
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(this.viewSelector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async isDisplayed(): Promise<boolean> {
|
||||
return isElementVisible(this.viewElement());
|
||||
}
|
||||
|
||||
protected viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.viewSelector);
|
||||
}
|
||||
|
||||
async numberOfLines(): Promise<number | undefined> {
|
||||
await this.waitForVisible();
|
||||
return this.monacoEditor.numberOfLines();
|
||||
}
|
||||
|
||||
async maxSeverityOfLineByLineNumber(lineNumber: number): Promise<'error' | 'warning' | 'info'> {
|
||||
await this.waitForVisible();
|
||||
const lineElement = await (await this.monacoEditor.line(lineNumber)).elementHandle();
|
||||
const contents = await lineElement?.$$('span > span.mtk1');
|
||||
if (!contents || contents.length < 1) {
|
||||
throw new Error(`Could not find contents of line number ${lineNumber}!`);
|
||||
}
|
||||
const severityClassNames = await Promise.all(contents.map(
|
||||
async content => (await content.getAttribute('class'))?.split(' ')[1]));
|
||||
|
||||
if (severityClassNames.includes('theia-output-error')) {
|
||||
return 'error';
|
||||
} else if (severityClassNames.includes('theia-output-warning')) {
|
||||
return 'warning';
|
||||
}
|
||||
return 'info';
|
||||
}
|
||||
|
||||
async textContentOfLineByLineNumber(lineNumber: number): Promise<string | undefined> {
|
||||
return this.monacoEditor.textContentOfLineByLineNumber(lineNumber);
|
||||
}
|
||||
}
|
||||
87
examples/playwright/src/theia-output-view.ts
Normal file
87
examples/playwright/src/theia-output-view.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaOutputViewChannel } from './theia-output-channel';
|
||||
import { TheiaView } from './theia-view';
|
||||
import { normalizeId } from './util';
|
||||
|
||||
const TheiaOutputViewData = {
|
||||
tabSelector: '#shell-tab-outputView',
|
||||
viewSelector: '#outputView',
|
||||
viewName: 'Output'
|
||||
};
|
||||
|
||||
export class TheiaOutputView extends TheiaView {
|
||||
constructor(app: TheiaApp) {
|
||||
super(TheiaOutputViewData, app);
|
||||
}
|
||||
|
||||
async isOutputChannelSelected(outputChannelName: string): Promise<boolean> {
|
||||
await this.activate();
|
||||
const contentPanel = await this.page.$('#theia-bottom-content-panel');
|
||||
if (contentPanel && (await contentPanel.isVisible())) {
|
||||
const channelList = await contentPanel.$('#outputChannelList');
|
||||
const selectedChannel = await channelList?.$('div.theia-select-component-label');
|
||||
if (selectedChannel && (await selectedChannel.textContent()) === outputChannelName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getOutputChannel(outputChannelName: string): Promise<TheiaOutputViewChannel | undefined> {
|
||||
await this.activate();
|
||||
const channel = new TheiaOutputViewChannel(
|
||||
{
|
||||
viewSelector: 'div.lm-Widget.theia-editor.lm-DockPanel-widget > div.monaco-editor',
|
||||
dataUri: normalizeId(`output:/${encodeURIComponent(outputChannelName)}`),
|
||||
channelName: outputChannelName
|
||||
},
|
||||
this
|
||||
);
|
||||
await channel.waitForVisible();
|
||||
if (await channel.isDisplayed()) {
|
||||
return channel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async selectOutputChannel(outputChannelName: string): Promise<boolean> {
|
||||
await this.activate();
|
||||
const contentPanel = await this.page.$('#theia-bottom-content-panel');
|
||||
if (contentPanel && (await contentPanel.isVisible())) {
|
||||
const channelSelectComponent = await contentPanel.$('#outputChannelList');
|
||||
if (!channelSelectComponent) {
|
||||
throw Error('Output Channel List not visible.');
|
||||
}
|
||||
// open output channel list component
|
||||
await channelSelectComponent.click();
|
||||
const channelContainer = await this.page.waitForSelector('#select-component-container > div.theia-select-component-dropdown');
|
||||
if (!channelContainer) {
|
||||
throw Error('Output Channel List could not be opened.');
|
||||
}
|
||||
const channels = await channelContainer.$$('div.theia-select-component-option-value');
|
||||
for (const channel of channels) {
|
||||
if (await channel.textContent() === outputChannelName) {
|
||||
await channel.click();
|
||||
}
|
||||
}
|
||||
return this.isOutputChannelSelected(outputChannelName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
29
examples/playwright/src/theia-page-object.ts
Normal file
29
examples/playwright/src/theia-page-object.ts
Normal file
@@ -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-only 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;
|
||||
}
|
||||
|
||||
}
|
||||
252
examples/playwright/src/theia-preference-view.ts
Normal file
252
examples/playwright/src/theia-preference-view.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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 = {
|
||||
Editor: {
|
||||
AutoSave: 'files.autoSave',
|
||||
RenderWhitespace: 'editor.renderWhitespace'
|
||||
},
|
||||
Explorer: {
|
||||
AutoReveal: 'explorer.autoReveal'
|
||||
},
|
||||
DiffEditor: {
|
||||
MaxComputationTime: 'diffEditor.maxComputationTime'
|
||||
},
|
||||
Files: {
|
||||
EnableTrash: 'files.enableTrash'
|
||||
}
|
||||
};
|
||||
|
||||
export const DefaultPreferences = {
|
||||
Editor: {
|
||||
AutoSave: {
|
||||
Off: 'off',
|
||||
AfterDelay: 'afterDelay',
|
||||
OnFocusChange: 'onFocusChange',
|
||||
OnWindowChange: 'onWindowChange'
|
||||
},
|
||||
RenderWhitespace: {
|
||||
None: 'none',
|
||||
Boundary: 'boundary',
|
||||
Selection: 'selection',
|
||||
Trailing: 'trailing',
|
||||
All: 'all'
|
||||
}
|
||||
},
|
||||
Explorer: {
|
||||
AutoReveal: {
|
||||
Enabled: true
|
||||
}
|
||||
},
|
||||
DiffEditor: {
|
||||
MaxComputationTime: '5000'
|
||||
},
|
||||
Files: {
|
||||
EnableTrash: {
|
||||
Enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export enum TheiaPreferenceScope {
|
||||
User = 'User',
|
||||
Workspace = 'Workspace'
|
||||
}
|
||||
|
||||
export class TheiaPreferenceView extends TheiaView {
|
||||
public customTimeout?: number;
|
||||
protected modificationIndicator = '.theia-mod-item-modified';
|
||||
protected optionSelectLabel = '.theia-select-component-label';
|
||||
protected optionSelectDropdown = '.theia-select-component-dropdown';
|
||||
protected optionSelectDropdownValue = '.theia-select-component-option-value';
|
||||
|
||||
constructor(app: TheiaApp) {
|
||||
super(TheiaSettingsViewData, app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param preferenceScope The preference scope (Workspace or User) to open the view for. Default is Workspace.
|
||||
* @param useMenu If true, the view will be opened via the main menu. If false,
|
||||
* the view will be opened via the quick command palette. Default is using the main menu.
|
||||
* @returns The TheiaPreferenceView page object instance.
|
||||
*/
|
||||
override async open(preferenceScope = TheiaPreferenceScope.Workspace, useMenu: boolean = true): Promise<TheiaView> {
|
||||
if (useMenu) {
|
||||
const mainMenu = await this.app.menuBar.openMenu('File');
|
||||
await (await mainMenu.menuItemByNamePath('Preferences', 'Settings'))?.click();
|
||||
} else {
|
||||
await this.app.quickCommandPalette.type('Preferences:');
|
||||
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.lm-TabBar-tabLabel:has-text("${scope}")`;
|
||||
}
|
||||
|
||||
async openPreferenceScope(scope: TheiaPreferenceScope): Promise<void> {
|
||||
await this.activate();
|
||||
const scopeTab = await this.page.waitForSelector(this.getScopeSelector(scope));
|
||||
await scopeTab.click();
|
||||
}
|
||||
|
||||
async getBooleanPreferenceByPath(sectionTitle: string, name: string): Promise<boolean> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.getBooleanPreferenceById(preferenceId);
|
||||
}
|
||||
|
||||
async getBooleanPreferenceById(preferenceId: string): Promise<boolean> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId);
|
||||
return element.isChecked();
|
||||
}
|
||||
|
||||
async setBooleanPreferenceByPath(sectionTitle: string, name: string, value: boolean): Promise<void> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.setBooleanPreferenceById(preferenceId, value);
|
||||
}
|
||||
|
||||
async setBooleanPreferenceById(preferenceId: string, value: boolean): Promise<void> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId);
|
||||
return value ? element.check() : element.uncheck();
|
||||
}
|
||||
|
||||
async getStringPreferenceByPath(sectionTitle: string, name: string): Promise<string> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.getStringPreferenceById(preferenceId);
|
||||
}
|
||||
|
||||
async getStringPreferenceById(preferenceId: string): Promise<string> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId);
|
||||
return element.evaluate(e => (e as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
async setStringPreferenceByPath(sectionTitle: string, name: string, value: string): Promise<void> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.setStringPreferenceById(preferenceId, value);
|
||||
}
|
||||
|
||||
async setStringPreferenceById(preferenceId: string, value: string): Promise<void> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId);
|
||||
return element.fill(value);
|
||||
}
|
||||
|
||||
async getOptionsPreferenceByPath(sectionTitle: string, name: string): Promise<string> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.getOptionsPreferenceById(preferenceId);
|
||||
}
|
||||
|
||||
async getOptionsPreferenceById(preferenceId: string): Promise<string> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId, this.optionSelectLabel);
|
||||
return element.evaluate(e => e.textContent ?? '');
|
||||
}
|
||||
|
||||
async setOptionsPreferenceByPath(sectionTitle: string, name: string, value: string): Promise<void> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.setOptionsPreferenceById(preferenceId, value);
|
||||
}
|
||||
|
||||
async setOptionsPreferenceById(preferenceId: string, value: string): Promise<void> {
|
||||
const element = await this.findPreferenceEditorById(preferenceId, this.optionSelectLabel);
|
||||
await element.click();
|
||||
const option = await this.page.waitForSelector(`${this.optionSelectDropdown} ${this.optionSelectDropdownValue}:has-text("${value}")`);
|
||||
await option.click();
|
||||
}
|
||||
|
||||
async resetPreferenceByPath(sectionTitle: string, name: string): Promise<void> {
|
||||
const preferenceId = await this.findPreferenceId(sectionTitle, name);
|
||||
return this.resetPreferenceById(preferenceId);
|
||||
}
|
||||
|
||||
async resetPreferenceById(preferenceId: string): Promise<void> {
|
||||
// this is just to fail if the preference doesn't exist at all
|
||||
await this.findPreferenceEditorById(preferenceId, '');
|
||||
const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
|
||||
await resetPreferenceButton.click();
|
||||
await this.waitForUnmodified(preferenceId);
|
||||
}
|
||||
|
||||
private async findPreferenceId(sectionTitle: string, name: string): Promise<string> {
|
||||
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, elementType: string = 'input'): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
const viewElement = await this.viewElement();
|
||||
const element = await viewElement?.waitForSelector(this.getPreferenceEditorSelector(preferenceId, elementType), { 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, elementType: string): string {
|
||||
return `${this.getPreferenceSelector(preferenceId)} ${elementType}`;
|
||||
}
|
||||
|
||||
private async findPreferenceResetButton(preferenceId: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
await this.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
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;
|
||||
}
|
||||
|
||||
async waitForModified(preferenceId: string): Promise<void> {
|
||||
await this.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}${this.modificationIndicator}`, { timeout: this.customTimeout });
|
||||
}
|
||||
|
||||
async waitForUnmodified(preferenceId: string): Promise<void> {
|
||||
await this.activate();
|
||||
const viewElement = await this.viewElement();
|
||||
await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}${this.modificationIndicator}`, { state: 'detached', timeout: this.customTimeout });
|
||||
}
|
||||
|
||||
private getPreferenceGutterSelector(preferenceId: string): string {
|
||||
return `${this.getPreferenceSelector(preferenceId)} .pref-context-gutter`;
|
||||
}
|
||||
}
|
||||
37
examples/playwright/src/theia-problem-indicator.ts
Normal file
37
examples/playwright/src/theia-problem-indicator.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaStatusIndicator } from './theia-status-indicator';
|
||||
|
||||
export class TheiaProblemIndicator extends TheiaStatusIndicator {
|
||||
id = 'problem-marker-status';
|
||||
|
||||
async numberOfProblems(): Promise<number> {
|
||||
const spans = await this.getSpans();
|
||||
return spans ? +await spans[1].innerText() : -1;
|
||||
}
|
||||
|
||||
async numberOfWarnings(): Promise<number> {
|
||||
const spans = await this.getSpans();
|
||||
return spans ? +await spans[3].innerText() : -1;
|
||||
}
|
||||
|
||||
protected async getSpans(): Promise<ElementHandle[] | undefined> {
|
||||
const handle = await this.getElementHandle();
|
||||
return handle?.$$('span');
|
||||
}
|
||||
}
|
||||
30
examples/playwright/src/theia-problem-view.ts
Normal file
30
examples/playwright/src/theia-problem-view.ts
Normal file
@@ -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-only 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);
|
||||
}
|
||||
}
|
||||
91
examples/playwright/src/theia-quick-command-palette.ts
Normal file
91
examples/playwright/src/theia-quick-command-palette.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
import { OSUtil, USER_KEY_TYPING_DELAY } from './util';
|
||||
|
||||
export class TheiaQuickCommandPalette extends TheiaPageObject {
|
||||
|
||||
selector = '.quick-input-widget';
|
||||
|
||||
async open(): Promise<void> {
|
||||
await this.page.keyboard.press(OSUtil.isMacOS ? 'Meta+Shift+p' : 'Control+Shift+p');
|
||||
await this.page.waitForSelector(this.selector);
|
||||
}
|
||||
|
||||
async hide(): Promise<void> {
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForSelector(this.selector, { state: 'hidden' });
|
||||
}
|
||||
|
||||
async isOpen(): Promise<boolean> {
|
||||
try {
|
||||
await this.page.waitForSelector(this.selector, { timeout: 5000 });
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async trigger(...commandName: string[]): Promise<void> {
|
||||
for (const command of commandName) {
|
||||
await this.triggerSingleCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
protected async triggerSingleCommand(commandName: string): Promise<void> {
|
||||
if (!await this.isOpen()) {
|
||||
this.open();
|
||||
}
|
||||
let selected = await this.selectedCommand();
|
||||
while (!(await selected?.innerText() === commandName)) {
|
||||
await this.page.keyboard.press('ArrowDown');
|
||||
selected = await this.selectedCommand();
|
||||
}
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
async type(value: string, confirm = false): Promise<void> {
|
||||
if (!await this.isOpen()) {
|
||||
this.open();
|
||||
}
|
||||
const input = this.page.locator(`${this.selector} .monaco-inputbox .input`);
|
||||
await input.focus();
|
||||
await input.pressSequentially(value, { delay: USER_KEY_TYPING_DELAY });
|
||||
if (confirm) {
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectedCommand(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
const command = await this.page.waitForSelector(this.selector);
|
||||
if (!command) {
|
||||
throw new Error('No selected command found!');
|
||||
}
|
||||
return command.$('.monaco-list-row.focused .monaco-highlighted-label');
|
||||
}
|
||||
|
||||
async visibleItems(): Promise<ElementHandle<SVGElement | HTMLElement>[]> {
|
||||
// FIXME rewrite with locators
|
||||
const command = await this.page.waitForSelector(this.selector);
|
||||
if (!command) {
|
||||
throw new Error('No selected command found!');
|
||||
}
|
||||
return command.$$('.monaco-highlighted-label');
|
||||
}
|
||||
|
||||
}
|
||||
35
examples/playwright/src/theia-rename-dialog.ts
Normal file
35
examples/playwright/src/theia-rename-dialog.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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<void> {
|
||||
const inputField = this.page.locator(`${this.blockSelector} .theia-input`);
|
||||
await inputField.selectText();
|
||||
await inputField.pressSequentially(newName, { delay: USER_KEY_TYPING_DELAY });
|
||||
}
|
||||
|
||||
async confirm(): Promise<void> {
|
||||
if (!await this.validationResult()) {
|
||||
throw new Error(`Unexpected validation error in TheiaRenameDialog: '${await this.getValidationText()}`);
|
||||
}
|
||||
await this.clickMainButton();
|
||||
}
|
||||
|
||||
}
|
||||
44
examples/playwright/src/theia-status-bar.ts
Normal file
44
examples/playwright/src/theia-status-bar.ts
Normal file
@@ -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-only 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<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.selector);
|
||||
}
|
||||
|
||||
async statusIndicator<T extends TheiaStatusIndicator>(statusIndicatorFactory: { new(app: TheiaApp): T }): Promise<T> {
|
||||
return new statusIndicatorFactory(this.app);
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
const statusBar = await this.statusBarElementHandle();
|
||||
return !!statusBar && statusBar.isVisible();
|
||||
}
|
||||
|
||||
}
|
||||
50
examples/playwright/src/theia-status-indicator.ts
Normal file
50
examples/playwright/src/theia-status-indicator.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
|
||||
export abstract class TheiaStatusIndicator extends TheiaPageObject {
|
||||
protected abstract id: string;
|
||||
|
||||
protected statusBarElementSelector = '#theia-statusBar div.element';
|
||||
|
||||
protected getSelectorForId(id: string): string {
|
||||
return `${this.statusBarElementSelector}#status-bar-${id}`;
|
||||
}
|
||||
|
||||
async waitForVisible(waitForDetached = false): Promise<void> {
|
||||
await this.page.waitForSelector(this.getSelectorForId(this.id), waitForDetached ? { state: 'detached' } : {});
|
||||
}
|
||||
|
||||
async getElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
const element = await this.page.$(this.getSelectorForId(this.id));
|
||||
if (element) {
|
||||
return element;
|
||||
}
|
||||
throw new Error('Could not find status bar element with ID ' + this.id);
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
try {
|
||||
const element = await this.getElementHandle();
|
||||
return element.isVisible();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
examples/playwright/src/theia-terminal.ts
Normal file
69
examples/playwright/src/theia-terminal.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only 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 { TheiaView } from './theia-view';
|
||||
|
||||
export class TheiaTerminal extends TheiaView {
|
||||
|
||||
constructor(tabId: string, app: TheiaApp) {
|
||||
super({
|
||||
tabSelector: `#shell-tab-terminal-${getTerminalId(tabId)}`,
|
||||
viewSelector: `#terminal-${getTerminalId(tabId)}`
|
||||
}, app);
|
||||
}
|
||||
|
||||
async submit(text: string): Promise<void> {
|
||||
await this.write(text);
|
||||
const input = await this.waitForInputArea();
|
||||
await input.press('Enter');
|
||||
}
|
||||
|
||||
async write(text: string): Promise<void> {
|
||||
await this.activate();
|
||||
const input = await this.waitForInputArea();
|
||||
await input.fill(text);
|
||||
}
|
||||
|
||||
async contents(): Promise<string> {
|
||||
await this.activate();
|
||||
await (await this.openContextMenu()).clickMenuItem('Select All');
|
||||
await (await this.openContextMenu()).clickMenuItem('Copy');
|
||||
return this.page.evaluate('navigator.clipboard.readText()');
|
||||
}
|
||||
|
||||
protected async openContextMenu(): Promise<TheiaMenu> {
|
||||
await this.activate();
|
||||
return TheiaContextMenu.open(this.app, () => this.waitForVisibleView());
|
||||
}
|
||||
|
||||
protected async waitForInputArea(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
const view = await this.waitForVisibleView();
|
||||
return view.waitForSelector('.xterm-helper-textarea');
|
||||
}
|
||||
|
||||
protected async waitForVisibleView(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(this.viewSelector, { state: 'visible' });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getTerminalId(tabId: string): string {
|
||||
return tabId.substring(tabId.lastIndexOf('-') + 1);
|
||||
}
|
||||
140
examples/playwright/src/theia-text-editor.ts
Normal file
140
examples/playwright/src/theia-text-editor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle, Locator } from '@playwright/test';
|
||||
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaEditor } from './theia-editor';
|
||||
import { normalizeId } from './util';
|
||||
import { TheiaMonacoEditor } from './theia-monaco-editor';
|
||||
|
||||
export class TheiaTextEditor extends TheiaEditor {
|
||||
|
||||
protected monacoEditor: TheiaMonacoEditor;
|
||||
|
||||
constructor(filePath: string, app: TheiaApp) {
|
||||
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
|
||||
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
|
||||
super({
|
||||
tabSelector: normalizeId(`#shell-tab-code-editor-opener:${app.workspace.pathAsUrl(filePath)}:1`),
|
||||
viewSelector: normalizeId(`#code-editor-opener:${app.workspace.pathAsUrl(filePath)}:1`) + '.theia-editor'
|
||||
}, app);
|
||||
this.monacoEditor = new TheiaMonacoEditor(this.page.locator(this.data.viewSelector), app);
|
||||
}
|
||||
|
||||
async numberOfLines(): Promise<number | undefined> {
|
||||
await this.activate();
|
||||
return this.monacoEditor.numberOfLines();
|
||||
}
|
||||
|
||||
async textContentOfLineByLineNumber(lineNumber: number): Promise<string | undefined> {
|
||||
return this.monacoEditor.textContentOfLineByLineNumber(lineNumber);
|
||||
}
|
||||
|
||||
async replaceLineWithLineNumber(text: string, lineNumber: number): Promise<void> {
|
||||
await this.selectLineWithLineNumber(lineNumber);
|
||||
await this.typeTextAndHitEnter(text);
|
||||
}
|
||||
|
||||
protected async typeTextAndHitEnter(text: string): Promise<void> {
|
||||
await this.page.keyboard.type(text);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
async selectLineWithLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
await this.activate();
|
||||
const lineElement = await this.monacoEditor.line(lineNumber);
|
||||
await this.selectLine(lineElement);
|
||||
return await lineElement.elementHandle() ?? undefined;
|
||||
}
|
||||
|
||||
async placeCursorInLineWithLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
await this.activate();
|
||||
const lineElement = await this.monacoEditor.line(lineNumber);
|
||||
await this.placeCursorInLine(lineElement);
|
||||
return await lineElement.elementHandle() ?? undefined;
|
||||
}
|
||||
|
||||
async deleteLineByLineNumber(lineNumber: number): Promise<void> {
|
||||
await this.selectLineWithLineNumber(lineNumber);
|
||||
await this.page.keyboard.press('Backspace');
|
||||
}
|
||||
|
||||
async textContentOfLineContainingText(text: string): Promise<string | undefined> {
|
||||
await this.activate();
|
||||
return this.monacoEditor.textContentOfLineContainingText(text);
|
||||
}
|
||||
|
||||
async replaceLineContainingText(newText: string, oldText: string): Promise<void> {
|
||||
await this.selectLineContainingText(oldText);
|
||||
await this.typeTextAndHitEnter(newText);
|
||||
}
|
||||
|
||||
async selectLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
await this.activate();
|
||||
const lineElement = await this.monacoEditor.lineWithText(text);
|
||||
await this.selectLine(lineElement);
|
||||
return await lineElement?.elementHandle() ?? undefined;
|
||||
}
|
||||
|
||||
async placeCursorInLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
|
||||
await this.activate();
|
||||
const lineElement = await this.monacoEditor.lineWithText(text);
|
||||
await this.placeCursorInLine(lineElement);
|
||||
return await lineElement?.elementHandle() ?? undefined;
|
||||
}
|
||||
|
||||
async deleteLineContainingText(text: string): Promise<void> {
|
||||
await this.selectLineContainingText(text);
|
||||
await this.page.keyboard.press('Backspace');
|
||||
}
|
||||
|
||||
async addTextToNewLineAfterLineContainingText(textContainedByExistingLine: string, newText: string): Promise<void> {
|
||||
const existingLine = await this.monacoEditor.lineWithText(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<void> {
|
||||
const existingLine = await this.monacoEditor.line(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 selectLine(lineLocator: Locator | undefined): Promise<void> {
|
||||
await lineLocator?.click({ clickCount: 3 });
|
||||
}
|
||||
|
||||
protected async placeCursorInLine(lineLocator: Locator | undefined): Promise<void> {
|
||||
await lineLocator?.click();
|
||||
}
|
||||
|
||||
protected async selectedSuggestion(): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||
return this.page.waitForSelector(this.viewSelector + ' .monaco-list-row.show-file-icons.focused');
|
||||
}
|
||||
|
||||
async getSelectedSuggestionText(): Promise<string> {
|
||||
const suggestion = await this.selectedSuggestion();
|
||||
const text = await suggestion.textContent();
|
||||
if (text === null) { throw new Error('Text content could not be found'); }
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
21
examples/playwright/src/theia-toggle-bottom-indicator.ts
Normal file
21
examples/playwright/src/theia-toggle-bottom-indicator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TheiaStatusIndicator } from './theia-status-indicator';
|
||||
|
||||
export class TheiaToggleBottomIndicator extends TheiaStatusIndicator {
|
||||
id = 'bottom-panel-toggle';
|
||||
}
|
||||
42
examples/playwright/src/theia-toolbar-item.ts
Normal file
42
examples/playwright/src/theia-toolbar-item.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
|
||||
export class TheiaToolbarItem extends TheiaPageObject {
|
||||
constructor(app: TheiaApp, protected element: ElementHandle<SVGElement | HTMLElement>) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async commandId(): Promise<string | null> {
|
||||
return this.element.getAttribute('id');
|
||||
}
|
||||
|
||||
async isEnabled(): Promise<boolean> {
|
||||
const child = await this.element.$(':first-child');
|
||||
const classAttribute = child && await child.getAttribute('class');
|
||||
if (classAttribute === undefined || classAttribute === null) {
|
||||
return false;
|
||||
}
|
||||
return classAttribute.includes('enabled');
|
||||
}
|
||||
|
||||
async trigger(): Promise<void> {
|
||||
await this.element.click();
|
||||
}
|
||||
}
|
||||
99
examples/playwright/src/theia-toolbar.ts
Normal file
99
examples/playwright/src/theia-toolbar.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { TheiaPageObject } from './theia-page-object';
|
||||
import { TheiaToolbarItem } from './theia-toolbar-item';
|
||||
|
||||
export class TheiaToolbar extends TheiaPageObject {
|
||||
selector = 'div#main-toolbar.lm-TabBar-toolbar';
|
||||
|
||||
protected async toolbarElementHandle(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.selector);
|
||||
}
|
||||
|
||||
async waitForVisible(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async isShown(): Promise<boolean> {
|
||||
const statusBar = await this.toolbarElementHandle();
|
||||
return !!statusBar && statusBar.isVisible();
|
||||
}
|
||||
|
||||
async show(): Promise<void> {
|
||||
if (!await this.isShown()) {
|
||||
await this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
async hide(): Promise<void> {
|
||||
if (await this.isShown()) {
|
||||
await this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
async toggle(): Promise<void> {
|
||||
const isShown = await this.isShown();
|
||||
const viewMenu = await this.app.menuBar.openMenu('View');
|
||||
await viewMenu.clickMenuItem('Toggle Toolbar');
|
||||
isShown ? await this.waitUntilHidden() : await this.waitUntilShown();
|
||||
}
|
||||
|
||||
async waitUntilHidden(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'hidden' });
|
||||
}
|
||||
|
||||
async waitUntilShown(): Promise<void> {
|
||||
await this.page.waitForSelector(this.selector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async toolbarItems(): Promise<TheiaToolbarItem[]> {
|
||||
const toolbarHandle = await this.toolbarElementHandle();
|
||||
if (!toolbarHandle) {
|
||||
return [];
|
||||
}
|
||||
const items = await toolbarHandle.$$(this.toolBarItemSelector());
|
||||
return items.map(element => new TheiaToolbarItem(this.app, element));
|
||||
}
|
||||
|
||||
async toolbarItemIds(): Promise<string[]> {
|
||||
const items = await this.toolbarItems();
|
||||
return this.toCommandIdArray(items);
|
||||
}
|
||||
|
||||
async toolBarItem(commandId: string): Promise<TheiaToolbarItem | undefined> {
|
||||
const toolbarHandle = await this.toolbarElementHandle();
|
||||
if (!toolbarHandle) {
|
||||
return undefined;
|
||||
}
|
||||
const item = await toolbarHandle.$(this.toolBarItemSelector(commandId));
|
||||
if (item) {
|
||||
return new TheiaToolbarItem(this.app, item);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected toolBarItemSelector(toolbarItemId = ''): string {
|
||||
return `div.toolbar-item${toolbarItemId ? `[id="${toolbarItemId}"]` : ''}`;
|
||||
}
|
||||
|
||||
protected async toCommandIdArray(items: TheiaToolbarItem[]): Promise<string[]> {
|
||||
const contents = items.map(item => item.commandId());
|
||||
const resolvedContents = await Promise.all(contents);
|
||||
return resolvedContents.filter(id => id !== undefined) as string[];
|
||||
}
|
||||
}
|
||||
81
examples/playwright/src/theia-tree-node.ts
Normal file
81
examples/playwright/src/theia-tree-node.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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';
|
||||
nodeSegmentLabelCssClass = '.theia-tree-compressed-label-part';
|
||||
expansionToggleCssClass = '.theia-ExpansionToggle';
|
||||
collapsedCssClass = '.theia-mod-collapsed';
|
||||
|
||||
constructor(protected elementHandle: ElementHandle<SVGElement | HTMLElement>, protected app: TheiaApp) { }
|
||||
|
||||
async label(): Promise<string | null> {
|
||||
const labelNode = await this.elementHandle.$(this.labelElementCssClass);
|
||||
if (!labelNode) {
|
||||
throw new Error('Cannot read label of ' + this.elementHandle);
|
||||
}
|
||||
return labelNode.textContent();
|
||||
}
|
||||
|
||||
async isCollapsed(): Promise<boolean> {
|
||||
return !! await this.elementHandle.$(this.collapsedCssClass);
|
||||
}
|
||||
|
||||
async isExpandable(): Promise<boolean> {
|
||||
return !! await this.elementHandle.$(this.expansionToggleCssClass);
|
||||
}
|
||||
|
||||
async expand(): Promise<void> {
|
||||
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 collapse(): Promise<void> {
|
||||
if (await this.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
const expansionToggle = await this.elementHandle.waitForSelector(this.expansionToggleCssClass);
|
||||
await expansionToggle.click();
|
||||
await this.elementHandle.waitForSelector(`${this.expansionToggleCssClass}${this.collapsedCssClass}`);
|
||||
}
|
||||
|
||||
async openContextMenu(): Promise<TheiaMenu> {
|
||||
return TheiaContextMenu.open(this.app, () => this.elementHandle.waitForSelector(this.labelElementCssClass));
|
||||
}
|
||||
|
||||
async openContextMenuOnSegment(nodeSegmentLabel: string): Promise<TheiaMenu> {
|
||||
const treeNodeLabel = await this.elementHandle.waitForSelector(this.labelElementCssClass);
|
||||
const treeNodeLabelSegments = await treeNodeLabel.$$(`span${this.nodeSegmentLabelCssClass}`);
|
||||
for (const segmentLabel of treeNodeLabelSegments) {
|
||||
if (await segmentLabel.textContent() === nodeSegmentLabel) {
|
||||
return TheiaContextMenu.open(this.app, () => Promise.resolve(segmentLabel));
|
||||
}
|
||||
}
|
||||
throw new Error('Could not find tree node segment label "' + nodeSegmentLabel + '"');
|
||||
}
|
||||
|
||||
}
|
||||
177
examples/playwright/src/theia-view.ts
Normal file
177
examples/playwright/src/theia-view.ts
Normal file
@@ -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-only 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<TheiaView> {
|
||||
if (!this.data.viewName) {
|
||||
throw new Error('View name must be specified to open via command palette');
|
||||
}
|
||||
await this.app.quickCommandPalette.type('View: Open View');
|
||||
await this.app.quickCommandPalette.trigger('View: Open View...', this.data.viewName);
|
||||
await this.waitForVisible();
|
||||
return this;
|
||||
}
|
||||
|
||||
async focus(): Promise<void> {
|
||||
await this.activate();
|
||||
const view = await this.viewElement();
|
||||
await view?.click();
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
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<void> {
|
||||
await this.page.waitForSelector(this.viewSelector, { state: 'visible' });
|
||||
}
|
||||
|
||||
async isTabVisible(): Promise<boolean> {
|
||||
return isElementVisible(this.tabElement());
|
||||
}
|
||||
|
||||
async isDisplayed(): Promise<boolean> {
|
||||
return isElementVisible(this.viewElement());
|
||||
}
|
||||
|
||||
async isActive(): Promise<boolean> {
|
||||
return await this.isTabVisible() && containsClass(this.tabElement(), 'lm-mod-current');
|
||||
}
|
||||
|
||||
async isClosable(): Promise<boolean> {
|
||||
return await this.isTabVisible() && containsClass(this.tabElement(), 'lm-mod-closable');
|
||||
}
|
||||
|
||||
async close(waitForClosed = true): Promise<void> {
|
||||
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.lm-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<void> {
|
||||
await this.page.waitForSelector(this.tabSelector, { state: 'detached' });
|
||||
}
|
||||
|
||||
async title(): Promise<string | undefined> {
|
||||
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.lm-TabBar-tabLabel'));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async isInSidePanel(): Promise<boolean> {
|
||||
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();
|
||||
const appAreaElement = tab?.$('xpath=../../../..');
|
||||
if (await containsClass(appAreaElement, 'theia-app-left')) {
|
||||
return 'left';
|
||||
}
|
||||
if (await containsClass(appAreaElement, 'theia-app-right')) {
|
||||
return 'right';
|
||||
}
|
||||
|
||||
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<TheiaMenu> {
|
||||
await this.activate();
|
||||
return TheiaContextMenu.open(this.app, () => this.page.waitForSelector(this.tabSelector));
|
||||
}
|
||||
|
||||
protected viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.viewSelector);
|
||||
}
|
||||
|
||||
protected tabElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
return this.page.$(this.tabSelector);
|
||||
}
|
||||
|
||||
}
|
||||
31
examples/playwright/src/theia-welcome-view.ts
Normal file
31
examples/playwright/src/theia-welcome-view.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Toro Cloud Pty Ltd 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-only WITH Classpath-exception-2.0
|
||||
|
||||
import { TheiaApp } from './theia-app';
|
||||
import { TheiaView } from './theia-view';
|
||||
import { normalizeId } from './util';
|
||||
|
||||
const TheiaWelcomeViewData = {
|
||||
tabSelector: normalizeId('#shell-tab-getting.started.widget'),
|
||||
viewSelector: normalizeId('#getting.started.widget'),
|
||||
viewName: 'Welcome'
|
||||
};
|
||||
|
||||
export class TheiaWelcomeView extends TheiaView {
|
||||
|
||||
constructor(app: TheiaApp) {
|
||||
super(TheiaWelcomeViewData, app);
|
||||
}
|
||||
}
|
||||
90
examples/playwright/src/theia-workspace.ts
Normal file
90
examples/playwright/src/theia-workspace.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import { join, resolve } from 'path';
|
||||
import { OSUtil } from './util';
|
||||
|
||||
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(join(OSUtil.tmpDir, '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 = resolve(process.cwd(), initPath);
|
||||
if (!fs.pathExistsSync(absoluteInitPath)) {
|
||||
throw Error('Workspace does not exist at ' + absoluteInitPath);
|
||||
}
|
||||
fs.copySync(absoluteInitPath, this.workspacePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the absolute path to the workspace location. */
|
||||
get path(): string {
|
||||
let workspacePath = this.workspacePath;
|
||||
if (OSUtil.isWindows) {
|
||||
// Drive letters in windows paths have to be lower case
|
||||
workspacePath = workspacePath.replace(/.:/, matchedChar => matchedChar.toLowerCase());
|
||||
}
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the workspace location
|
||||
* as it would be returned by URI.path.
|
||||
*/
|
||||
get pathAsPathComponent(): string {
|
||||
let path = this.path;
|
||||
if (!path.startsWith(OSUtil.fileSeparator)) {
|
||||
path = OSUtil.fileSeparator + path;
|
||||
}
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file URL for the given subpath relative to the workspace location.
|
||||
*/
|
||||
pathAsUrl(subpath: string): string {
|
||||
let path = resolve(this.path, subpath);
|
||||
if (!path.startsWith(OSUtil.fileSeparator)) {
|
||||
path = OSUtil.fileSeparator + path;
|
||||
}
|
||||
path = path.replace(/\\/g, '/').replace(/:/g, '%3A');
|
||||
return 'file://' + path;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
fs.emptyDirSync(this.workspacePath);
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
fs.removeSync(this.workspacePath);
|
||||
}
|
||||
|
||||
}
|
||||
83
examples/playwright/src/util.ts
Normal file
83
examples/playwright/src/util.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElementHandle } from '@playwright/test';
|
||||
import { tmpdir, platform } from 'os';
|
||||
import { sep } from 'path';
|
||||
|
||||
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<SVGElement | HTMLElement>[]): Promise<string[]> {
|
||||
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<ElementHandle<SVGElement | HTMLElement> | null>): Promise<string | undefined> {
|
||||
const element = await elementPromise;
|
||||
if (!element) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await element.textContent();
|
||||
return content ? content : undefined;
|
||||
}
|
||||
|
||||
export async function containsClass(elementPromise: Promise<ElementHandle<SVGElement | HTMLElement> | null> | undefined, cssClass: string): Promise<boolean> {
|
||||
return elementContainsClass(await elementPromise, cssClass);
|
||||
}
|
||||
|
||||
export async function elementContainsClass(element: ElementHandle<SVGElement | HTMLElement> | null | undefined, cssClass: string): Promise<boolean> {
|
||||
if (element) {
|
||||
const classValue = await element.getAttribute('class');
|
||||
if (classValue) {
|
||||
return classValue?.split(' ').includes(cssClass);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function isElementVisible(elementPromise: Promise<ElementHandle<SVGElement | HTMLElement> | null>): Promise<boolean> {
|
||||
const element = await elementPromise;
|
||||
return element ? element.isVisible() : false;
|
||||
}
|
||||
|
||||
export async function elementId(element: ElementHandle<SVGElement | HTMLElement>): Promise<string> {
|
||||
const id = await element.getAttribute('id');
|
||||
if (id === null) { throw new Error('Could not get ID of ' + element); }
|
||||
return id;
|
||||
}
|
||||
|
||||
export namespace OSUtil {
|
||||
export const isWindows = platform() === 'win32';
|
||||
export const isMacOS = platform() === 'darwin';
|
||||
// The platform-specific file separator '\' or '/'.
|
||||
export const fileSeparator = sep;
|
||||
// The platform-specific location of the temporary directory.
|
||||
export const tmpDir = tmpdir();
|
||||
}
|
||||
16
examples/playwright/tsconfig.json
Normal file
16
examples/playwright/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../dev-packages/cli"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user