From 2e1e3d3e5c4ad54bb5b1dda82990dc9768868c9f Mon Sep 17 00:00:00 2001 From: Philip Langer Date: Fri, 3 Feb 2023 09:58:45 +0100 Subject: [PATCH] First working version Contributed on behalf of STMicroelectronics. Change-Id: I7c817b00262229ab096ecbbacca5c465b9fa6302 --- examples/playwright/src/index.ts | 1 + .../src/tests/fixtures/theia-fixture.ts | 25 --- .../playwright/src/tests/theia-app.test.ts | 15 +- .../src/tests/theia-electron-app.test.ts | 56 ++++++ .../src/tests/theia-explorer-view.test.ts | 21 ++- .../src/tests/theia-main-menu.test.ts | 22 ++- .../src/tests/theia-preference-view.test.ts | 19 +- .../src/tests/theia-problems-view.test.ts | 19 +- .../src/tests/theia-quick-command.test.ts | 23 ++- .../src/tests/theia-sample-app.test.ts | 20 ++- .../src/tests/theia-status-bar.test.ts | 20 ++- .../src/tests/theia-text-editor.test.ts | 21 ++- .../src/tests/theia-workspace.test.ts | 17 +- examples/playwright/src/theia-app-loader.ts | 167 ++++++++++++++++++ examples/playwright/src/theia-app.ts | 51 +++--- .../playwright/src/theia-explorer-view.ts | 1 + 16 files changed, 371 insertions(+), 127 deletions(-) delete mode 100644 examples/playwright/src/tests/fixtures/theia-fixture.ts create mode 100644 examples/playwright/src/tests/theia-electron-app.test.ts create mode 100644 examples/playwright/src/theia-app-loader.ts diff --git a/examples/playwright/src/index.ts b/examples/playwright/src/index.ts index ca00881b5322f..5dd9ebe6a9a75 100644 --- a/examples/playwright/src/index.ts +++ b/examples/playwright/src/index.ts @@ -16,6 +16,7 @@ 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'; diff --git a/examples/playwright/src/tests/fixtures/theia-fixture.ts b/examples/playwright/src/tests/fixtures/theia-fixture.ts deleted file mode 100644 index 3c1d27efde1c7..0000000000000 --- a/examples/playwright/src/tests/fixtures/theia-fixture.ts +++ /dev/null @@ -1,25 +0,0 @@ -// ***************************************************************************** -// 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 test, { Page } from '@playwright/test'; - -export let page: Page; - -test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); -}); - -export default test; diff --git a/examples/playwright/src/tests/theia-app.test.ts b/examples/playwright/src/tests/theia-app.test.ts index 157e279899d83..7316f5afeec3b 100644 --- a/examples/playwright/src/tests/theia-app.test.ts +++ b/examples/playwright/src/tests/theia-app.test.ts @@ -14,20 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { TheiaApp } from '../theia-app'; - -import { expect } from '@playwright/test'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; +import { expect, test } from '@playwright/test'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; test.describe('Theia Application', () => { - test('should load', async () => { - app = await TheiaApp.load(page); - }); - - test('should show main content panel', async () => { + test('should load and should show main content panel', async ({ page }) => { + const app = await TheiaBrowserAppLoader.load(page); expect(await app.isMainContentPanelVisible()).toBe(true); }); diff --git a/examples/playwright/src/tests/theia-electron-app.test.ts b/examples/playwright/src/tests/theia-electron-app.test.ts new file mode 100644 index 0000000000000..afbdbeb36c2bb --- /dev/null +++ b/examples/playwright/src/tests/theia-electron-app.test.ts @@ -0,0 +1,56 @@ +// ***************************************************************************** +// 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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { expect, test } from '@playwright/test'; +import { TheiaExplorerView } from '../theia-explorer-view'; +import { TheiaAboutDialog } from '../theia-about-dialog'; +import { ElectronLaunchOptions, TheiaElectronAppLoader } from '../theia-app-loader'; +import { TheiaWorkspace } from '../theia-workspace'; + +test.describe('Theia Electron Application', () => { + + test('should load and show main content panel', async () => { + const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + const app = await TheiaElectronAppLoader.load(new ElectronLaunchOptions('../electron', '../../plugins'), ws); + expect(await app.isMainContentPanelVisible()).toBe(true); + + const quickCommand = app.quickCommandPalette; + + await quickCommand.open(); + expect(await quickCommand.isOpen()).toBe(true); + + await quickCommand.open(); + await quickCommand.type('About'); + await quickCommand.trigger('About'); + expect(await quickCommand.isOpen()).toBe(false); + const aboutDialog = new TheiaAboutDialog(app); + expect(await aboutDialog.isVisible()).toBe(true); + await aboutDialog.close(); + expect(await aboutDialog.isVisible()).toBe(false); + + await quickCommand.type('Select All'); + await quickCommand.trigger('Select All'); + expect(await quickCommand.isOpen()).toBe(false); + + await quickCommand.open(); + await quickCommand.type('Toggle Explorer'); + await quickCommand.trigger('Toggle Explorer View'); + expect(await quickCommand.isOpen()).toBe(false); + const explorerView = new TheiaExplorerView(app); + expect(await explorerView.isDisplayed()).toBe(true); + }); + +}); diff --git a/examples/playwright/src/tests/theia-explorer-view.test.ts b/examples/playwright/src/tests/theia-explorer-view.test.ts index 7977acfb2ad8a..4bd95033398e5 100644 --- a/examples/playwright/src/tests/theia-explorer-view.test.ts +++ b/examples/playwright/src/tests/theia-explorer-view.test.ts @@ -14,24 +14,31 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { TheiaApp } from '../theia-app'; import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view'; import { TheiaWorkspace } from '../theia-workspace'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; -let explorer: TheiaExplorerView; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Explorer View', () => { - test.beforeAll(async () => { + let app: TheiaApp; + let explorer: TheiaExplorerView; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - app = await TheiaApp.load(page, ws); + app = await TheiaBrowserAppLoader.load(page, ws); 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); diff --git a/examples/playwright/src/tests/theia-main-menu.test.ts b/examples/playwright/src/tests/theia-main-menu.test.ts index aad8595efcd83..2ae45c8cf6fc2 100644 --- a/examples/playwright/src/tests/theia-main-menu.test.ts +++ b/examples/playwright/src/tests/theia-main-menu.test.ts @@ -14,21 +14,29 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; -import { OSUtil } from '../util'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { TheiaMenuBar } from '../theia-main-menu'; -import test, { page } from './fixtures/theia-fixture'; - -let menuBar: TheiaMenuBar; +import { OSUtil } from '../util'; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Main Menu', () => { - test.beforeAll(async () => { - const app = await TheiaApp.load(page); + let app: TheiaApp; + let menuBar: TheiaMenuBar; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); 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'); diff --git a/examples/playwright/src/tests/theia-preference-view.test.ts b/examples/playwright/src/tests/theia-preference-view.test.ts index f6e3491c4bab0..b9fc4d0967430 100644 --- a/examples/playwright/src/tests/theia-preference-view.test.ts +++ b/examples/playwright/src/tests/theia-preference-view.test.ts @@ -14,17 +14,24 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Preference View', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + let app: TheiaApp; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); + }); + + test.afterAll(async () => { + await app.page.close(); }); test('should be visible and active after being opened', async () => { diff --git a/examples/playwright/src/tests/theia-problems-view.test.ts b/examples/playwright/src/tests/theia-problems-view.test.ts index 7fc79cc2b118a..48dd8919acfe9 100644 --- a/examples/playwright/src/tests/theia-problems-view.test.ts +++ b/examples/playwright/src/tests/theia-problems-view.test.ts @@ -14,17 +14,24 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { TheiaProblemsView } from '../theia-problem-view'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Problems View', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + let app: TheiaApp; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); + }); + + test.afterAll(async () => { + await app.page.close(); }); test('should be visible and active after being opened', async () => { diff --git a/examples/playwright/src/tests/theia-quick-command.test.ts b/examples/playwright/src/tests/theia-quick-command.test.ts index fcd2d026ad62e..b5392ef0afb3f 100644 --- a/examples/playwright/src/tests/theia-quick-command.test.ts +++ b/examples/playwright/src/tests/theia-quick-command.test.ts @@ -14,25 +14,32 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; +import { TheiaBrowserAppLoader } 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'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; -let quickCommand: TheiaQuickCommandPalette; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Quick Command', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + let app: TheiaApp; + let quickCommand: TheiaQuickCommandPalette; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); 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); @@ -43,6 +50,7 @@ test.describe('Theia Quick Command', () => { }); test('should trigger \'About\' command after typing', async () => { + await quickCommand.open(); await quickCommand.type('About'); await quickCommand.trigger('About'); expect(await quickCommand.isOpen()).toBe(false); @@ -57,6 +65,7 @@ test.describe('Theia Quick Command', () => { }); test('should trigger \'Toggle Explorer View\' command after typing', async () => { + await quickCommand.open(); await quickCommand.type('Toggle Explorer'); await quickCommand.trigger('Toggle Explorer View'); expect(await quickCommand.isOpen()).toBe(false); diff --git a/examples/playwright/src/tests/theia-sample-app.test.ts b/examples/playwright/src/tests/theia-sample-app.test.ts index 356d80f454662..38456caf2a582 100644 --- a/examples/playwright/src/tests/theia-sample-app.test.ts +++ b/examples/playwright/src/tests/theia-sample-app.test.ts @@ -14,10 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { TheiaToolbar } from '../theia-toolbar'; -import test, { page } from './fixtures/theia-fixture'; +import { TheiaWorkspace } from '../theia-workspace'; class TheiaSampleApp extends TheiaApp { protected toolbar = new TheiaToolbar(this); @@ -35,12 +36,19 @@ class TheiaSampleApp extends TheiaApp { } } -let app: TheiaSampleApp; - +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Sample Application', () => { - test('should load', async () => { - app = await TheiaApp.loadApp(page, TheiaSampleApp); + let app: TheiaSampleApp; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page, new TheiaWorkspace(), TheiaSampleApp); + }); + + test.afterAll(async () => { + await app.page.close(); }); test('should start with visible toolbar', async () => { diff --git a/examples/playwright/src/tests/theia-status-bar.test.ts b/examples/playwright/src/tests/theia-status-bar.test.ts index 20f575ed7cad1..2e1f1b9f90061 100644 --- a/examples/playwright/src/tests/theia-status-bar.test.ts +++ b/examples/playwright/src/tests/theia-status-bar.test.ts @@ -14,23 +14,31 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } 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'; -import test, { page } from './fixtures/theia-fixture'; - -let statusBar: TheiaStatusBar; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Status Bar', () => { - test.beforeAll(async () => { - const app = await TheiaApp.load(page); + let app: TheiaApp; + let statusBar: TheiaStatusBar; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); statusBar = app.statusBar; }); + test.afterAll(async () => { + await app.page.close(); + }); + test('should show status bar', async () => { expect(await statusBar.isVisible()).toBe(true); }); diff --git a/examples/playwright/src/tests/theia-text-editor.test.ts b/examples/playwright/src/tests/theia-text-editor.test.ts index d47daf09bb129..4789048cd4b24 100644 --- a/examples/playwright/src/tests/theia-text-editor.test.ts +++ b/examples/playwright/src/tests/theia-text-editor.test.ts @@ -14,20 +14,23 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; -import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view'; +import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view'; import { TheiaTextEditor } from '../theia-text-editor'; import { TheiaWorkspace } from '../theia-workspace'; -import test, { page } from './fixtures/theia-fixture'; - -let app: TheiaApp; +// the tests in this file reuse a page to run faster and thus are executed serially +test.describe.configure({ mode: 'serial' }); test.describe('Theia Text Editor', () => { - test.beforeAll(async () => { + let app: TheiaApp; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - app = await TheiaApp.load(page, ws); + app = await TheiaBrowserAppLoader.load(page, ws); // set auto-save preference to off const preferenceView = await app.openPreferences(TheiaPreferenceView); @@ -35,6 +38,10 @@ test.describe('Theia Text Editor', () => { 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); diff --git a/examples/playwright/src/tests/theia-workspace.test.ts b/examples/playwright/src/tests/theia-workspace.test.ts index 02ae0f94c4f9b..e9d4efab5d1f5 100644 --- a/examples/playwright/src/tests/theia-workspace.test.ts +++ b/examples/playwright/src/tests/theia-workspace.test.ts @@ -14,24 +14,23 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { expect } from '@playwright/test'; -import { TheiaApp } from '../theia-app'; +import { expect, test } from '@playwright/test'; +import { TheiaBrowserAppLoader } from '../theia-app-loader'; import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view'; import { TheiaWorkspace } from '../theia-workspace'; -import test, { page } from './fixtures/theia-fixture'; test.describe('Theia Workspace', () => { - test('should be initialized empty by default', async () => { - const app = await TheiaApp.load(page); + test('should be initialized empty by default', async ({ page }) => { + const app = await TheiaBrowserAppLoader.load(page); const explorer = await app.openView(TheiaExplorerView); const fileStatElements = await explorer.visibleFileStatNodes(DOT_FILES_FILTER); expect(fileStatElements.length).toBe(0); }); - test('should be initialized with the contents of a file location', async () => { + test('should be initialized with the contents of a file location', async ({ page }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - const app = await TheiaApp.load(page, ws); + const app = await TheiaBrowserAppLoader.load(page, ws); const explorer = await app.openView(TheiaExplorerView); // resources/sample-files1 contains two folders and one file expect(await explorer.existsDirectoryNode('sampleFolder')).toBe(true); @@ -39,9 +38,9 @@ test.describe('Theia Workspace', () => { expect(await explorer.existsFileNode('sample.txt')).toBe(true); }); - test('should be initialized with the contents of multiple file locations', async () => { + test('should be initialized with the contents of multiple file locations', async ({ page }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1', 'src/tests/resources/sample-files2']); - const app = await TheiaApp.load(page, ws); + const app = await TheiaBrowserAppLoader.load(page, ws); const explorer = await app.openView(TheiaExplorerView); // resources/sample-files1 contains two folders and one file expect(await explorer.existsDirectoryNode('sampleFolder')).toBe(true); diff --git a/examples/playwright/src/theia-app-loader.ts b/examples/playwright/src/theia-app-loader.ts new file mode 100644 index 0000000000000..bea923a8da7bd --- /dev/null +++ b/examples/playwright/src/theia-app-loader.ts @@ -0,0 +1,167 @@ +// ***************************************************************************** +// 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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Page, PlaywrightWorkerArgs, _electron as electron } from '@playwright/test'; +import { TheiaApp, TheiaAppMainPageObjects } from './theia-app'; +import { TheiaWorkspace } from './theia-workspace'; + +export interface TheiaAppFactory { + new(page: Page, initialWorkspace?: TheiaWorkspace, mainPageObjects?: TheiaAppMainPageObjects): T; +} + +function theiaAppFactory(factory?: TheiaAppFactory): TheiaAppFactory { + return (factory ?? TheiaApp) as TheiaAppFactory; +} + +function initializeWorkspace(initialWorkspace?: TheiaWorkspace): TheiaWorkspace { + const workspace = initialWorkspace ? initialWorkspace : new TheiaWorkspace(); + workspace.initialize(); + return workspace; +} + +export class TheiaBrowserAppLoader { + + static async load( + page: Page, + initialWorkspace?: TheiaWorkspace, + factory?: TheiaAppFactory + ): Promise { + const workspace = initializeWorkspace(initialWorkspace); + return this.createAndLoad(page, workspace, factory); + } + + protected static async createAndLoad( + page: Page, + workspace: TheiaWorkspace, + factory?: TheiaAppFactory + ): Promise { + const appFactory = theiaAppFactory(factory); + const app = new appFactory(page, workspace); + await this.loadOrReload(app, '/#' + app.workspace.urlEncodedPath); + await app.waitForShellAndInitialized(); + return app; + } + + protected static async loadOrReload(app: TheiaApp, url: string): Promise { + 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(); + } + } + } + +} + +export class TheiaElectronAppLoader { + + static async load( + launchOptions: ElectronLaunchOptions | object, + initialWorkspace?: TheiaWorkspace, + factory?: TheiaAppFactory, + ): Promise { + const workspace = initializeWorkspace(initialWorkspace); + const playwrightOptions = this.toPlaywrightOptions(launchOptions, initialWorkspace); + const electronApp = await electron.launch(playwrightOptions); + const page = await electronApp.firstWindow(); + const appFactory = theiaAppFactory(factory); + const app = new appFactory(page, workspace); + await app.waitForShellAndInitialized(); + return app; + } + + protected static toPlaywrightOptions( + electronLaunchOptions: ElectronLaunchOptions | object, + workspace?: TheiaWorkspace + ): object { + if (electronLaunchOptions instanceof ElectronLaunchOptions) { + return electronLaunchOptions.playwrightOptions(workspace); + } + return electronLaunchOptions; + } +} + +export class ElectronLaunchOptions { + + constructor( + protected readonly electronAppPath: string, + protected readonly pluginsPath?: string, + protected readonly additionalArgs: string[] = ['--no-cluster'] + ) { } + + playwrightOptions(workspace?: TheiaWorkspace): object { + const executablePath = this.electronAppPath + '/node_modules/.bin/electron'; + const args: string[] = []; + args.push(this.electronAppPath); + args.push(...this.additionalArgs); + args.push(`--app-project-path=${this.electronAppPath}`); + if (this.pluginsPath) { + args.push(`--plugins=local-dir:${this.pluginsPath}`); + }; + if (workspace) { + args.push(workspace.path); + } + return { executablePath, args }; + } +} + +// TODO this is just a sketch, we need a proper way to configure tests and pass this configuration to the `TheiaAppLoader`: + +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, + } +} + +export class TheiaAppLoader { + static async load( + args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs, + initialWorkspace?: TheiaWorkspace, + factory?: TheiaAppFactory, + ): Promise { + if (args.useElectron) { + return TheiaAppLoader.launchElectron(args, factory, initialWorkspace); + } + const page = await args.browser.newPage(); + return TheiaBrowserAppLoader.load(page, initialWorkspace, factory); + } + + private static launchElectron( + args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs, + factory?: TheiaAppFactory, + initialWorkspace?: TheiaWorkspace + ): Promise { + const electronConfig = args.useElectron; + 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 ?? new ElectronLaunchOptions(appPath, pluginsPath).playwrightOptions(initialWorkspace); + return TheiaElectronAppLoader.load(launchOptions, initialWorkspace, factory); + } +} diff --git a/examples/playwright/src/theia-app.ts b/examples/playwright/src/theia-app.ts index 901a7a172dbc2..2d40079a88a4c 100644 --- a/examples/playwright/src/theia-app.ts +++ b/examples/playwright/src/theia-app.ts @@ -35,49 +35,40 @@ export const DefaultTheiaAppData: TheiaAppData = { shellSelector: '.theia-ApplicationShell' }; +export class TheiaAppMainPageObjects { + statusBar: new (page: TheiaApp) => TheiaStatusBar = TheiaStatusBar; + quickCommandPalette: new (page: TheiaApp) => TheiaQuickCommandPalette = TheiaQuickCommandPalette; + menuBar: new (page: TheiaApp) => TheiaMenuBar = TheiaMenuBar; +} + export class TheiaApp { - readonly statusBar = new TheiaStatusBar(this); - readonly quickCommandPalette = new TheiaQuickCommandPalette(this); - readonly menuBar = new TheiaMenuBar(this); - public workspace: TheiaWorkspace; + statusBar: TheiaStatusBar; + quickCommandPalette: TheiaQuickCommandPalette; + menuBar: TheiaMenuBar; - static async load(page: Page, initialWorkspace?: TheiaWorkspace): Promise { - return this.loadApp(page, TheiaApp, initialWorkspace); - } + protected appData = DefaultTheiaAppData; - static async loadApp(page: Page, appFactory: { new(page: Page, initialWorkspace?: TheiaWorkspace): T }, initialWorkspace?: TheiaWorkspace): Promise { - const app = new appFactory(page, initialWorkspace); - await app.load(); - return app; + public constructor( + public page: Page, + public workspace: TheiaWorkspace, + mainPageObjects: TheiaAppMainPageObjects = new TheiaAppMainPageObjects() + ) { + this.statusBar = new mainPageObjects.statusBar(this); + this.quickCommandPalette = new mainPageObjects.quickCommandPalette(this); + this.menuBar = new mainPageObjects.menuBar(this); } - public constructor(public page: Page, initialWorkspace?: TheiaWorkspace, protected appData = DefaultTheiaAppData) { - this.workspace = initialWorkspace ? initialWorkspace : new TheiaWorkspace(); - this.workspace.initialize(); + async isShellVisible(): Promise { + return this.page.isVisible(this.appData.shellSelector); } - protected async load(): Promise { - await this.loadOrReload(this.page, '/#' + this.workspace.urlEncodedPath); + async waitForShellAndInitialized(): Promise { await this.page.waitForSelector(this.appData.loadingSelector, { state: 'detached' }); await this.page.waitForSelector(this.appData.shellSelector); await this.waitForInitialized(); } - protected async loadOrReload(page: Page, url: string): Promise { - if (page.url() === url) { - await page.reload(); - } else { - const wasLoadedAlready = await page.isVisible(this.appData.shellSelector); - await page.goto(url); - if (wasLoadedAlready) { - // Theia doesn't refresh on URL change only - // So we need to reload if the app was already loaded before - await page.reload(); - } - } - } - async isMainContentPanelVisible(): Promise { const contentPanel = await this.page.$('#theia-main-content-panel'); return !!contentPanel && contentPanel.isVisible(); diff --git a/examples/playwright/src/theia-explorer-view.ts b/examples/playwright/src/theia-explorer-view.ts index 9d70214086628..df3b9e6c8bee0 100644 --- a/examples/playwright/src/theia-explorer-view.ts +++ b/examples/playwright/src/theia-explorer-view.ts @@ -182,6 +182,7 @@ export class TheiaExplorerView extends TheiaView { 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 {