From 2b018b0f017d3bb8fb0ea2f23506106a313dcf76 Mon Sep 17 00:00:00 2001 From: Olaf Lessenich Date: Fri, 10 Feb 2023 11:58:54 +0100 Subject: [PATCH] feat: improve playwright support for electron This patch adds the possibility to disable natively rendered elements by setting the environment variable THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS to 1. We need this functionality for testing menu actions or use cases that involve file choosers using electron playwright. When the environment variable is set, the app loader enforces the rendering of HTML menus that playwright can access. Contributed on behalf of STMicroelectronics Signed-off-by: Olaf Lessenich --- .../configs/playwright.ci.config.ts | 26 ++++++ .../playwright/configs/playwright.config.ts | 37 ++++++++ .../configs/playwright.debug.config.ts | 27 ++++++ .../configs/playwright.headful.config.ts | 30 ++++++ .../playwright/configs/ui-tests.eslintrc.json | 7 ++ .../configs/ui-tests.playwright.eslintrc.json | 6 ++ examples/playwright/package.json | 5 +- .../playwright/src/tests/theia-app.test.ts | 6 +- .../src/tests/theia-electron-app.test.ts | 91 +++++++++++++++++-- .../src/tests/theia-explorer-view.test.ts | 7 +- .../src/tests/theia-main-menu.test.ts | 7 +- .../src/tests/theia-output-view.test.ts | 12 +-- .../src/tests/theia-preference-view.test.ts | 7 +- .../src/tests/theia-problems-view.test.ts | 7 +- .../src/tests/theia-quick-command.test.ts | 7 +- .../src/tests/theia-sample-app.test.ts | 7 +- .../src/tests/theia-status-bar.test.ts | 7 +- .../src/tests/theia-terminal-view.test.ts | 8 +- .../src/tests/theia-text-editor.test.ts | 7 +- .../src/tests/theia-toolbar.test.ts | 8 +- .../src/tests/theia-workspace.test.ts | 31 +++++-- examples/playwright/src/theia-app-loader.ts | 22 ++++- examples/playwright/src/theia-workspace.ts | 13 +-- .../electron-main-application.ts | 12 ++- .../electron-file-dialog-module.ts | 33 ++++++- .../src/electron-browser/preload.ts | 2 + .../src/electron-common/electron-api.ts | 1 + 27 files changed, 346 insertions(+), 87 deletions(-) create mode 100644 examples/playwright/configs/playwright.ci.config.ts create mode 100644 examples/playwright/configs/playwright.config.ts create mode 100644 examples/playwright/configs/playwright.debug.config.ts create mode 100644 examples/playwright/configs/playwright.headful.config.ts create mode 100644 examples/playwright/configs/ui-tests.eslintrc.json create mode 100644 examples/playwright/configs/ui-tests.playwright.eslintrc.json diff --git a/examples/playwright/configs/playwright.ci.config.ts b/examples/playwright/configs/playwright.ci.config.ts new file mode 100644 index 0000000000000..a30b65f7227d0 --- /dev/null +++ b/examples/playwright/configs/playwright.ci.config.ts @@ -0,0 +1,26 @@ +// ***************************************************************************** +// Copyright (C) 2022 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { PlaywrightTestConfig } from '@playwright/test'; +import baseConfig from './playwright.config'; + +const ciConfig: PlaywrightTestConfig = { + ...baseConfig, + workers: 1, + retries: 1 +}; + +export default ciConfig; diff --git a/examples/playwright/configs/playwright.config.ts b/examples/playwright/configs/playwright.config.ts new file mode 100644 index 0000000000000..c2f850f868334 --- /dev/null +++ b/examples/playwright/configs/playwright.config.ts @@ -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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '../lib/tests', + testMatch: ['**/*.js'], + workers: 2, + // Timeout for each test in milliseconds. + timeout: 60 * 1000, + use: { + baseURL: 'http://localhost:3000', + browserName: 'chromium', + screenshot: 'only-on-failure' + }, + preserveOutput: 'failures-only', + reporter: [ + ['list'], + ['allure-playwright'] + ] +}; + +export default config; diff --git a/examples/playwright/configs/playwright.debug.config.ts b/examples/playwright/configs/playwright.debug.config.ts new file mode 100644 index 0000000000000..41ac56377b9ab --- /dev/null +++ b/examples/playwright/configs/playwright.debug.config.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// Copyright (C) 2021 logi.cals GmbH, EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { PlaywrightTestConfig } from '@playwright/test'; + +import baseConfig from './playwright.config'; + +const debugConfig: PlaywrightTestConfig = { + ...baseConfig, + workers: 1, + timeout: 15000000 +}; + +export default debugConfig; diff --git a/examples/playwright/configs/playwright.headful.config.ts b/examples/playwright/configs/playwright.headful.config.ts new file mode 100644 index 0000000000000..8c87926fa2aa8 --- /dev/null +++ b/examples/playwright/configs/playwright.headful.config.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// Copyright (C) 2021 logi.cals GmbH, EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { PlaywrightTestConfig } from '@playwright/test'; + +import baseConfig from './playwright.config'; + +const headfulConfig: PlaywrightTestConfig = { + ...baseConfig, + workers: 1, + use: { + ...baseConfig.use, + headless: false + } +}; + +export default headfulConfig; diff --git a/examples/playwright/configs/ui-tests.eslintrc.json b/examples/playwright/configs/ui-tests.eslintrc.json new file mode 100644 index 0000000000000..735abed1c450a --- /dev/null +++ b/examples/playwright/configs/ui-tests.eslintrc.json @@ -0,0 +1,7 @@ +{ + // override existing rules for ui-tests package + "rules": { + "no-undef": "off", // disabled due to 'browser', '$', '$$' + "no-unused-expressions": "off" + } +} diff --git a/examples/playwright/configs/ui-tests.playwright.eslintrc.json b/examples/playwright/configs/ui-tests.playwright.eslintrc.json new file mode 100644 index 0000000000000..15f12466adae8 --- /dev/null +++ b/examples/playwright/configs/ui-tests.playwright.eslintrc.json @@ -0,0 +1,6 @@ +{ + // override existing rules for ui-tests playwright package + "rules": { + "no-null/no-null": "off" + } +} diff --git a/examples/playwright/package.json b/examples/playwright/package.json index 33e4af44653cf..d6c409fd3c38c 100644 --- a/examples/playwright/package.json +++ b/examples/playwright/package.json @@ -19,8 +19,9 @@ "lint": "eslint -c ./.eslintrc.js --ext .ts ./src", "lint:fix": "eslint -c ./.eslintrc.js --ext .ts ./src --fix", "playwright:install": "playwright install chromium", - "ui-tests": "yarn build && playwright test", - "ui-tests-headful": "yarn build && playwright test --headed", + "ui-tests": "yarn build && playwright test --config=./configs/playwright.config.ts", + "ui-tests-ci": "yarn build && playwright test --config=./configs/playwright.ci.config.ts", + "ui-tests-headful": "yarn build && playwright test --config=./configs/playwright.headful.config.ts", "ui-tests-report-generate": "allure generate ./allure-results --clean -o allure-results/allure-report", "ui-tests-report": "yarn ui-tests-report-generate && allure open allure-results/allure-report" }, diff --git a/examples/playwright/src/tests/theia-app.test.ts b/examples/playwright/src/tests/theia-app.test.ts index 7316f5afeec3b..eeaed8cafda31 100644 --- a/examples/playwright/src/tests/theia-app.test.ts +++ b/examples/playwright/src/tests/theia-app.test.ts @@ -15,12 +15,12 @@ // ***************************************************************************** import { expect, test } from '@playwright/test'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; test.describe('Theia Application', () => { - test('should load and should show main content panel', async ({ page }) => { - const app = await TheiaBrowserAppLoader.load(page); + test('should load and should show main content panel', async ({ playwright, browser }) => { + const app = await TheiaAppLoader.load({ playwright, browser }); 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 index afbdbeb36c2bb..3d5d9e3f27b79 100644 --- a/examples/playwright/src/tests/theia-electron-app.test.ts +++ b/examples/playwright/src/tests/theia-electron-app.test.ts @@ -11,46 +11,117 @@ // 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 +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only 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 { TheiaAppLoader } from '../theia-app-loader'; import { TheiaWorkspace } from '../theia-workspace'; +import { TheiaApp } from '../theia-app'; +import { TheiaMenuBar } from 'src/theia-main-menu'; +test.describe.configure({ mode: 'serial' }); test.describe('Theia Electron Application', () => { + let app: TheiaApp; + let ws: TheiaWorkspace; + let menuBar: TheiaMenuBar; + + test.beforeAll(async ({ playwright, browser }) => { + ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + const args = { + useElectron: { + electronAppPath: '../electron', + pluginsPath: '../../plugins' + }, + playwright: playwright, + browser: browser + + } + ; + app = await TheiaAppLoader.load(args, ws); + menuBar = app.menuBar; + }); + + test.afterAll(async () => { + await app.page.close(); + }); + 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; + 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.getByRole('button', { name: 'OK' }).click(); + expect(await aboutDialog.isVisible()).toBe(false); + }); - await quickCommand.open(); - expect(await quickCommand.isOpen()).toBe(true); + test('open file via file menu and cancel', async () => { + await (await menuBar.openMenu('File')).clickMenuItem('Open File...'); + const fileDialog = await app.page.waitForSelector('div[class="dialogBlock"]'); + expect(await fileDialog.isVisible()).toBe(true); + await app.page.getByRole('button', { name: 'Cancel' }).click(); + expect(await fileDialog.isVisible()).toBe(false); + }); + + test('open sample.txt via file menu', async () => { + const menuEntry = 'Open File...'; + + await (await 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.getByRole('button', { name: 'Open' }).click(); + + const span = await app.page.waitForSelector('span:has-text("content line 2")'); + expect(await span.isVisible()).toBe(true); + }); + + test('open about dialog using command', async () => { + const quickCommand = app.quickCommandPalette; 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(); + await aboutDialog.page.getByRole('button', { name: 'OK' }).click(); expect(await aboutDialog.isVisible()).toBe(false); + }); + test('select all using command', async () => { + const quickCommand = app.quickCommandPalette; await quickCommand.type('Select All'); await quickCommand.trigger('Select All'); expect(await quickCommand.isOpen()).toBe(false); + }); + test('toggle explorer view using command', async () => { + const quickCommand = app.quickCommandPalette; 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); + await quickCommand.open(); + await quickCommand.type('Toggle Explorer'); + await quickCommand.trigger('Toggle Explorer View'); + expect(await explorerView.isDisplayed()).toBe(false); }); + test('toggle explorer view using menu', async () => { + await (await menuBar.openMenu('View')).clickMenuItem('Explorer'); + const explorerView = new TheiaExplorerView(app); + expect(await explorerView.isDisplayed()).toBe(true); + await (await menuBar.openMenu('View')).clickMenuItem('Explorer'); + expect(await explorerView.isDisplayed()).toBe(false); + }); }); + diff --git a/examples/playwright/src/tests/theia-explorer-view.test.ts b/examples/playwright/src/tests/theia-explorer-view.test.ts index 4bd95033398e5..c3fba18dc91ae 100644 --- a/examples/playwright/src/tests/theia-explorer-view.test.ts +++ b/examples/playwright/src/tests/theia-explorer-view.test.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { expect, test } from '@playwright/test'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaApp } from '../theia-app'; import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view'; import { TheiaWorkspace } from '../theia-workspace'; @@ -27,10 +27,9 @@ test.describe('Theia Explorer View', () => { let app: TheiaApp; let explorer: TheiaExplorerView; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); + test.beforeAll(async ({ playwright, browser }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - app = await TheiaBrowserAppLoader.load(page, ws); + app = await TheiaAppLoader.load({ playwright, browser }, ws); explorer = await app.openView(TheiaExplorerView); await explorer.waitForVisibleFileNodes(); }); diff --git a/examples/playwright/src/tests/theia-main-menu.test.ts b/examples/playwright/src/tests/theia-main-menu.test.ts index 2ae45c8cf6fc2..edc4a9bb8076a 100644 --- a/examples/playwright/src/tests/theia-main-menu.test.ts +++ b/examples/playwright/src/tests/theia-main-menu.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaMenuBar } from '../theia-main-menu'; import { OSUtil } from '../util'; @@ -27,9 +27,8 @@ test.describe('Theia Main Menu', () => { let app: TheiaApp; let menuBar: TheiaMenuBar; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); menuBar = app.menuBar; }); diff --git a/examples/playwright/src/tests/theia-output-view.test.ts b/examples/playwright/src/tests/theia-output-view.test.ts index c991081d751b0..cf77a368b67dc 100644 --- a/examples/playwright/src/tests/theia-output-view.test.ts +++ b/examples/playwright/src/tests/theia-output-view.test.ts @@ -14,20 +14,18 @@ // 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 { TheiaOutputViewChannel } from 'src/theia-output-channel'; import { TheiaApp } from '../theia-app'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaOutputView } from '../theia-output-view'; -import test, { page } from './fixtures/theia-fixture'; -let app: TheiaApp; -let outputView: TheiaOutputView; -let testChannel: TheiaOutputViewChannel; +let app: TheiaApp; let outputView: TheiaOutputView; let testChannel: TheiaOutputViewChannel; test.describe('Theia Output View', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); }); test('should open the output view and check if is visible and active', async () => { diff --git a/examples/playwright/src/tests/theia-preference-view.test.ts b/examples/playwright/src/tests/theia-preference-view.test.ts index b9fc4d0967430..91c4f1bba67e3 100644 --- a/examples/playwright/src/tests/theia-preference-view.test.ts +++ b/examples/playwright/src/tests/theia-preference-view.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view'; // the tests in this file reuse a page to run faster and thus are executed serially @@ -25,9 +25,8 @@ test.describe('Preference View', () => { let app: TheiaApp; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); }); test.afterAll(async () => { diff --git a/examples/playwright/src/tests/theia-problems-view.test.ts b/examples/playwright/src/tests/theia-problems-view.test.ts index 48dd8919acfe9..11b7a688fb23a 100644 --- a/examples/playwright/src/tests/theia-problems-view.test.ts +++ b/examples/playwright/src/tests/theia-problems-view.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaProblemsView } from '../theia-problem-view'; // the tests in this file reuse a page to run faster and thus are executed serially @@ -25,9 +25,8 @@ test.describe('Theia Problems View', () => { let app: TheiaApp; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); }); test.afterAll(async () => { diff --git a/examples/playwright/src/tests/theia-quick-command.test.ts b/examples/playwright/src/tests/theia-quick-command.test.ts index b5392ef0afb3f..10478432be14f 100644 --- a/examples/playwright/src/tests/theia-quick-command.test.ts +++ b/examples/playwright/src/tests/theia-quick-command.test.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { expect, test } from '@playwright/test'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaAboutDialog } from '../theia-about-dialog'; import { TheiaApp } from '../theia-app'; import { TheiaExplorerView } from '../theia-explorer-view'; @@ -30,9 +30,8 @@ test.describe('Theia Quick Command', () => { let app: TheiaApp; let quickCommand: TheiaQuickCommandPalette; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); quickCommand = app.quickCommandPalette; }); diff --git a/examples/playwright/src/tests/theia-sample-app.test.ts b/examples/playwright/src/tests/theia-sample-app.test.ts index 38456caf2a582..d9afb7e1de4bf 100644 --- a/examples/playwright/src/tests/theia-sample-app.test.ts +++ b/examples/playwright/src/tests/theia-sample-app.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaToolbar } from '../theia-toolbar'; import { TheiaWorkspace } from '../theia-workspace'; @@ -42,9 +42,8 @@ test.describe('Theia Sample Application', () => { let app: TheiaSampleApp; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page, new TheiaWorkspace(), TheiaSampleApp); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }, new TheiaWorkspace(), TheiaSampleApp); }); test.afterAll(async () => { diff --git a/examples/playwright/src/tests/theia-status-bar.test.ts b/examples/playwright/src/tests/theia-status-bar.test.ts index 2e1f1b9f90061..f9bb801640499 100644 --- a/examples/playwright/src/tests/theia-status-bar.test.ts +++ b/examples/playwright/src/tests/theia-status-bar.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { TheiaNotificationIndicator } from '../theia-notification-indicator'; import { TheiaProblemIndicator } from '../theia-problem-indicator'; import { TheiaStatusBar } from '../theia-status-bar'; @@ -29,9 +29,8 @@ test.describe('Theia Status Bar', () => { let app: TheiaApp; let statusBar: TheiaStatusBar; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - app = await TheiaBrowserAppLoader.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); statusBar = app.statusBar; }); diff --git a/examples/playwright/src/tests/theia-terminal-view.test.ts b/examples/playwright/src/tests/theia-terminal-view.test.ts index 807e35a3c9e15..403a2586d1fc2 100644 --- a/examples/playwright/src/tests/theia-terminal-view.test.ts +++ b/examples/playwright/src/tests/theia-terminal-view.test.ts @@ -14,19 +14,19 @@ // 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 { TheiaAppLoader } from '../theia-app-loader'; 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 () => { + test.beforeAll(async ({ playwright, browser }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - app = await TheiaApp.load(page, ws); + app = await TheiaAppLoader.load({ playwright, browser }, ws); }); test('should be possible to open a new terminal', async () => { diff --git a/examples/playwright/src/tests/theia-text-editor.test.ts b/examples/playwright/src/tests/theia-text-editor.test.ts index 4789048cd4b24..1227e05782633 100644 --- a/examples/playwright/src/tests/theia-text-editor.test.ts +++ b/examples/playwright/src/tests/theia-text-editor.test.ts @@ -16,7 +16,7 @@ import { expect, test } from '@playwright/test'; import { TheiaApp } from '../theia-app'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +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'; @@ -27,10 +27,9 @@ test.describe('Theia Text Editor', () => { let app: TheiaApp; - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); + test.beforeAll(async ({ playwright, browser }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - app = await TheiaBrowserAppLoader.load(page, ws); + app = await TheiaAppLoader.load({ playwright, browser }, ws); // set auto-save preference to off const preferenceView = await app.openPreferences(TheiaPreferenceView); diff --git a/examples/playwright/src/tests/theia-toolbar.test.ts b/examples/playwright/src/tests/theia-toolbar.test.ts index 1d5234c09208d..9091612c4d7d4 100644 --- a/examples/playwright/src/tests/theia-toolbar.test.ts +++ b/examples/playwright/src/tests/theia-toolbar.test.ts @@ -14,18 +14,18 @@ // 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 { TheiaAppLoader } from '../theia-app-loader'; import { TheiaToolbar } from '../theia-toolbar'; -import test, { page } from './fixtures/theia-fixture'; let app: TheiaApp; let toolbar: TheiaToolbar; test.describe('Theia Toolbar', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + test.beforeAll(async ({ playwright, browser }) => { + app = await TheiaAppLoader.load({ playwright, browser }); toolbar = new TheiaToolbar(app); }); diff --git a/examples/playwright/src/tests/theia-workspace.test.ts b/examples/playwright/src/tests/theia-workspace.test.ts index e9d4efab5d1f5..c6df6e72cc191 100644 --- a/examples/playwright/src/tests/theia-workspace.test.ts +++ b/examples/playwright/src/tests/theia-workspace.test.ts @@ -15,22 +15,22 @@ // ***************************************************************************** import { expect, test } from '@playwright/test'; -import { TheiaBrowserAppLoader } from '../theia-app-loader'; +import { TheiaAppLoader } from '../theia-app-loader'; import { DOT_FILES_FILTER, TheiaExplorerView } from '../theia-explorer-view'; import { TheiaWorkspace } from '../theia-workspace'; test.describe('Theia Workspace', () => { - test('should be initialized empty by default', async ({ page }) => { - const app = await TheiaBrowserAppLoader.load(page); + test('should be initialized empty by default', async ({ playwright, browser }) => { + 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); }); - test('should be initialized with the contents of a file location', async ({ page }) => { + test('should be initialized with the contents of a file location', async ({ playwright, browser }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); - const app = await TheiaBrowserAppLoader.load(page, ws); + 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); @@ -38,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 ({ page }) => { + test('should be initialized with the contents of multiple file locations', async ({ playwright, browser }) => { const ws = new TheiaWorkspace(['src/tests/resources/sample-files1', 'src/tests/resources/sample-files2']); - const app = await TheiaBrowserAppLoader.load(page, ws); + 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); @@ -50,4 +50,21 @@ test.describe('Theia Workspace', () => { expect(await explorer.existsFileNode('another-sample.txt')).toBe(true); }); + test('open sample.txt via file menu', async ({ playwright, browser }) => { + const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + const app = await TheiaAppLoader.load({ playwright, browser }, ws); + const menuEntry = '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.getByRole('button', { name: 'Open' }).click(); + + const span = await app.page.waitForSelector('span:has-text("content line 2")'); + expect(await span.isVisible()).toBe(true); + }); + }); diff --git a/examples/playwright/src/theia-app-loader.ts b/examples/playwright/src/theia-app-loader.ts index bea923a8da7bd..674ba4ac812c7 100644 --- a/examples/playwright/src/theia-app-loader.ts +++ b/examples/playwright/src/theia-app-loader.ts @@ -11,12 +11,15 @@ // 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 +// 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 * as path from 'path'; +import * as fs from 'fs'; import { TheiaApp, TheiaAppMainPageObjects } from './theia-app'; import { TheiaWorkspace } from './theia-workspace'; +import { OSUtil } from './util'; export interface TheiaAppFactory { new(page: Page, initialWorkspace?: TheiaWorkspace, mainPageObjects?: TheiaAppMainPageObjects): T; @@ -32,7 +35,7 @@ function initializeWorkspace(initialWorkspace?: TheiaWorkspace): TheiaWorkspace return workspace; } -export class TheiaBrowserAppLoader { +class TheiaBrowserAppLoader { static async load( page: Page, @@ -71,7 +74,7 @@ export class TheiaBrowserAppLoader { } -export class TheiaElectronAppLoader { +class TheiaElectronAppLoader { static async load( launchOptions: ElectronLaunchOptions | object, @@ -108,7 +111,16 @@ export class ElectronLaunchOptions { ) { } playwrightOptions(workspace?: TheiaWorkspace): object { - const executablePath = this.electronAppPath + '/node_modules/.bin/electron'; + let executablePath = path.normalize(path.join(this.electronAppPath, 'node_modules/.bin/electron')); + if (OSUtil.isWindows) { + executablePath += '.cmd'; + } + if (!fs.existsSync(executablePath)) { + const errorMsg = `executablePath: ${executablePath} does not exist`; + console.log(errorMsg); + throw new Error(errorMsg); + } + const args: string[] = []; args.push(this.electronAppPath); args.push(...this.additionalArgs); @@ -119,6 +131,8 @@ export class ElectronLaunchOptions { if (workspace) { args.push(workspace.path); } + process.env.THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS = '1'; + console.log(`Launching Electron: ${executablePath} ${args.join(' ')}`); return { executablePath, args }; } } diff --git a/examples/playwright/src/theia-workspace.ts b/examples/playwright/src/theia-workspace.ts index f7385bca5f84f..ab1f9c391ef17 100644 --- a/examples/playwright/src/theia-workspace.ts +++ b/examples/playwright/src/theia-workspace.ts @@ -15,6 +15,7 @@ // ***************************************************************************** import * as fs from 'fs-extra'; +import * as path from 'path'; import { resolve } from 'path'; import { OSUtil, urlEncodePath } from './util'; @@ -29,7 +30,7 @@ export class TheiaWorkspace { * @param {string[]} pathOfFilesToInitialize Path to files or folders that shall be copied to the workspace */ constructor(protected pathOfFilesToInitialize?: string[]) { - this.workspacePath = fs.mkdtempSync(`${OSUtil.tmpDir}${OSUtil.fileSeparator}cloud-ws-`); + this.workspacePath = fs.mkdtempSync(path.join(OSUtil.tmpDir, 'cloud-ws-')); } /** Performs the file system operations preparing the workspace location synchronously. */ @@ -46,15 +47,7 @@ export class TheiaWorkspace { } get path(): string { - let workspacePath = this.workspacePath; - if (!OSUtil.osStartsWithFileSeparator(this.workspacePath)) { - workspacePath = `${OSUtil.fileSeparator}${workspacePath}`; - } - if (OSUtil.isWindows) { - // Drive letters in windows paths have to be lower case - workspacePath = workspacePath.replace(/.:/, matchedChar => matchedChar.toLowerCase()); - } - return workspacePath; + return path.normalize(this.workspacePath); } get urlEncodedPath(): string { diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index bf43b2cb025a4..4836a4133d52c 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -192,6 +192,7 @@ export class ElectronMainApplication { } async start(config: FrontendApplicationConfig): Promise { + const args = this.processArgv.getProcessArgvWithoutBin(process.argv); this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; this._config = config; this.hookApplicationEvents(); @@ -202,12 +203,15 @@ export class ElectronMainApplication { await this.startContributions(); await this.launch({ secondInstance: false, - argv: this.processArgv.getProcessArgvWithoutBin(process.argv), + argv: args, cwd: process.cwd() }); } protected getTitleBarStyle(config: FrontendApplicationConfig): 'native' | 'custom' { + if ('THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS' in process.env && process.env.THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS === '1') { + return 'custom'; + } if (isOSX) { return 'native'; } @@ -321,6 +325,9 @@ export class ElectronMainApplication { async openDefaultWindow(): Promise { const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow()]); + if (!this.useNativeWindowFrame) { + electronWindow.setMenuBarVisibility(false); + } electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true)); return electronWindow; } @@ -328,6 +335,9 @@ export class ElectronMainApplication { protected async openWindowWithWorkspace(workspacePath: string): Promise { const options = await this.getLastWindowOptions(); const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow(options)]); + if (!this.useNativeWindowFrame) { + electronWindow.setMenuBarVisibility(false); + } electronWindow.loadURL(uri.withFragment(encodeURI(workspacePath)).toString(true)); return electronWindow; } diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts index 42b5dd29fb6f9..586b7c489eece 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts @@ -15,10 +15,37 @@ // ***************************************************************************** import { ContainerModule } from '@theia/core/shared/inversify'; -import { FileDialogService } from '../../browser/file-dialog/file-dialog-service'; +import { DefaultFileDialogService, FileDialogService } from '../../browser/file-dialog/file-dialog-service'; import { ElectronFileDialogService } from './electron-file-dialog-service'; +import { LocationListRenderer, LocationListRendererFactory, LocationListRendererOptions } from '../../browser/location'; +import { FileDialogHiddenFilesToggleRenderer, HiddenFilesToggleRendererFactory } from '../../browser/file-dialog/file-dialog-hidden-files-renderer'; +import { FileDialogTree } from '../../browser/file-dialog/file-dialog-tree'; +import { FileDialogTreeFiltersRenderer, FileDialogTreeFiltersRendererFactory, FileDialogTreeFiltersRendererOptions } from '../../browser/file-dialog'; export default new ContainerModule(bind => { - bind(ElectronFileDialogService).toSelf().inSingletonScope(); - bind(FileDialogService).toService(ElectronFileDialogService); + if (window.electronTheiaFilesystem.useNativeDialogs) { + bind(ElectronFileDialogService).toSelf().inSingletonScope(); + bind(FileDialogService).toService(ElectronFileDialogService); + } else { + bind(DefaultFileDialogService).toSelf().inSingletonScope(); + bind(FileDialogService).toService(DefaultFileDialogService); + bind(LocationListRendererFactory).toFactory(context => (options: LocationListRendererOptions) => { + const childContainer = context.container.createChild(); + childContainer.bind(LocationListRendererOptions).toConstantValue(options); + childContainer.bind(LocationListRenderer).toSelf().inSingletonScope(); + return childContainer.get(LocationListRenderer); + }); + bind(FileDialogTreeFiltersRendererFactory).toFactory(context => (options: FileDialogTreeFiltersRendererOptions) => { + const childContainer = context.container.createChild(); + childContainer.bind(FileDialogTreeFiltersRendererOptions).toConstantValue(options); + childContainer.bind(FileDialogTreeFiltersRenderer).toSelf().inSingletonScope(); + return childContainer.get(FileDialogTreeFiltersRenderer); + }); + bind(HiddenFilesToggleRendererFactory).toFactory(({ container }) => (fileDialogTree: FileDialogTree) => { + const child = container.createChild(); + child.bind(FileDialogTree).toConstantValue(fileDialogTree); + child.bind(FileDialogHiddenFilesToggleRenderer).toSelf().inSingletonScope(); + return child.get(FileDialogHiddenFilesToggleRenderer); + }); + } }); diff --git a/packages/filesystem/src/electron-browser/preload.ts b/packages/filesystem/src/electron-browser/preload.ts index 7a5f0d3ffd400..64cb7f79c6af9 100644 --- a/packages/filesystem/src/electron-browser/preload.ts +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -21,10 +21,12 @@ import { ipcRenderer, contextBridge } from '@theia/core/electron-shared/electron const api: TheiaFilesystemAPI = { showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_OPEN, options), showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_SAVE, options), + useNativeDialogs: !('THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS' in process.env && process.env.THEIA_ELECTRON_DISABLE_NATIVE_ELEMENTS === '1') }; export function preload(): void { console.log('exposing theia filesystem electron api'); contextBridge.exposeInMainWorld('electronTheiaFilesystem', api); + } diff --git a/packages/filesystem/src/electron-common/electron-api.ts b/packages/filesystem/src/electron-common/electron-api.ts index 59d59d3a14403..5145f308dfced 100644 --- a/packages/filesystem/src/electron-common/electron-api.ts +++ b/packages/filesystem/src/electron-common/electron-api.ts @@ -43,6 +43,7 @@ export interface SaveDialogOptions { export interface TheiaFilesystemAPI { showOpenDialog(options: OpenDialogOptions): Promise; showSaveDialog(options: SaveDialogOptions): Promise; + useNativeDialogs: boolean; } declare global {