deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View 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
View File

@@ -0,0 +1,4 @@
allure-results
test-results
playwright-report
.tmp.cfg

View 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%;'>
![Theia System Testing in Action](./docs/images/teaser.gif)
</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>

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,7 @@
{
// override existing rules for ui-tests package
"rules": {
"no-undef": "off", // disabled due to 'browser', '$', '$$'
"no-unused-expressions": "off"
}
}

View File

@@ -0,0 +1,6 @@
{
// override existing rules for ui-tests playwright package
"rules": {
"no-null/no-null": "off"
}
}

View 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).

View 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);
```

View 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%;'>
![Theia](./images/debug-example.gif)
</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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View 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"
}

View 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';

View File

@@ -0,0 +1,3 @@
{
"files.autoSave": "off"
}

View File

@@ -0,0 +1,18 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,4 @@
this is just a sample file
content line 2
content line 3
content line 4

View File

@@ -0,0 +1 @@
this is just another sample file

View 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);
});
});

View File

@@ -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();
}
});
});

View 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);
});
});

View 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);
});
});

View 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);
});
});

View 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;
}

View 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');
});
});

View 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;
}
});
});

View 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();
});
});

View 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');
});
});

View 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);
});
});

View 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);
});
});

View 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');
});
});

View 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);
});
});

View 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);
});
});

View 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();
});
});

View 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();
}
}

View 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);
}
}

View 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
}
}

View 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;
}
}

View 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` });
}
}

View 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);
}
}
}

View 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 }
);
}
}

View 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}`;
}
}

View 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();
}
}

View 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);
}
}

View 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> {
// [ ] &nbsp; => \u00a0 -- NO-BREAK SPACE
// [·] &middot; => \u00b7 -- MIDDLE DOT
// [] &zwnj; => \u200c -- ZERO WIDTH NON-JOINER
return content.replace(/[\u00a0\u00b7]/g, ' ').replace(/[\u200c]/g, '');
}
}

View 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();
}
}

View 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);
}
}

View 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' });
}
}

View 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();
}
}
}

View 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 */);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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`;
}
}

View 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');
}
}

View 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);
}
}

View 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');
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}
}

View 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);
}

View 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;
}
}

View 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';
}

View 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();
}
}

View 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[];
}
}

View 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 + '"');
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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();
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../../dev-packages/cli"
}
]
}