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