From c33553806b7dc3d17d4a820627104280bc84ab3a Mon Sep 17 00:00:00 2001 From: Philip Langer Date: Wed, 5 Apr 2023 18:13:16 +0200 Subject: [PATCH] playwright: add page object for terminal xterm uses a canvas to show the terminal contents. Thus, we use a workaround of selecting all contents and copying into the clipboard to read the output of the terminal. Contributed on behalf of STMicroelectronics Change-Id: I2840425ced9dfebf24f97b4302b44d73a6c9741c --- examples/playwright/playwright.config.ts | 5 +- .../src/tests/theia-terminal-view.test.ts | 85 +++++++++++++++++++ examples/playwright/src/theia-app.ts | 38 ++++++++- examples/playwright/src/theia-terminal.ts | 69 +++++++++++++++ 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 examples/playwright/src/tests/theia-terminal-view.test.ts create mode 100644 examples/playwright/src/theia-terminal.ts diff --git a/examples/playwright/playwright.config.ts b/examples/playwright/playwright.config.ts index 598183b8eb747..4f4a48a3ba7da 100644 --- a/examples/playwright/playwright.config.ts +++ b/examples/playwright/playwright.config.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2021 logi.cals GmbH, EclipseSource and others. +// 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 @@ -31,7 +31,8 @@ export default defineConfig({ baseURL: 'http://localhost:3000', browserName: 'chromium', screenshot: 'only-on-failure', - viewport: { width: 1920, height: 1080 } + permissions: ['clipboard-read'], + viewport: { width: 1920, height: 1080 }, }, snapshotDir: './src/tests/snapshots', expect: { diff --git a/examples/playwright/src/tests/theia-terminal-view.test.ts b/examples/playwright/src/tests/theia-terminal-view.test.ts new file mode 100644 index 0000000000000..fce2883f9b662 --- /dev/null +++ b/examples/playwright/src/tests/theia-terminal-view.test.ts @@ -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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { expect } from '@playwright/test'; +import { TheiaApp } from '../theia-app'; +import { TheiaWorkspace } from '../theia-workspace'; +import test, { page } from './fixtures/theia-fixture'; +import { TheiaTerminal } from '../theia-terminal'; + +let app: TheiaApp; + +test.describe('Theia Terminal View', () => { + + test.beforeAll(async () => { + const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + app = await TheiaApp.load(page, ws); + }); + + 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.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'); + }); + +}); diff --git a/examples/playwright/src/theia-app.ts b/examples/playwright/src/theia-app.ts index 00ccf3bbcc33d..5cb93716de39d 100644 --- a/examples/playwright/src/theia-app.ts +++ b/examples/playwright/src/theia-app.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2021 logi.cals GmbH, EclipseSource and others. +// 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 @@ -21,6 +21,7 @@ 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'; @@ -147,6 +148,41 @@ export class TheiaApp { return editor; } + async openTerminal(terminalFactory: { new(id: string, app: TheiaApp): T }): Promise { + 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): Promise { + const tabIdsBefore = await this.visibleTabIds(); + await command(); + return (await this.waitForNewTabs(tabIdsBefore)).filter(item => !tabIdsBefore.includes(item)); + } + + protected async waitForNewTabs(tabIds: string[]): Promise { + 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 { + const tabs = await this.page.$$('.p-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 { // empty by default diff --git a/examples/playwright/src/theia-terminal.ts b/examples/playwright/src/theia-terminal.ts new file mode 100644 index 0000000000000..18d874393ff77 --- /dev/null +++ b/examples/playwright/src/theia-terminal.ts @@ -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 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 { + await this.write(text); + const input = await this.waitForInputArea(); + await input.press('Enter'); + } + + async write(text: string): Promise { + await this.activate(); + const input = await this.waitForInputArea(); + await input.type(text); + } + + async contents(): Promise { + 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 { + await this.activate(); + return TheiaContextMenu.open(this.app, () => this.waitForVisibleView()); + } + + protected async waitForInputArea(): Promise> { + const view = await this.waitForVisibleView(); + return view.waitForSelector('.xterm-helper-textarea'); + } + + protected async waitForVisibleView(): Promise> { + return this.page.waitForSelector(this.viewSelector, { state: 'visible' }); + } + +} + +function getTerminalId(tabId: string): string { + return tabId.substring(tabId.lastIndexOf('-') + 1); +}