From f59e791cf304559313d8b8cdd9089f70528d07e7 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 +- .../src/tests/theia-electron-app.test.ts | 74 +++++++++++++++++-- .../src/tests/theia-output-view.test.ts | 13 ++-- .../src/tests/theia-terminal-view.test.ts | 9 ++- .../src/tests/theia-toolbar.test.ts | 9 ++- .../src/tests/theia-workspace.test.ts | 17 +++++ examples/playwright/src/theia-app-loader.ts | 16 +++- examples/playwright/src/theia-workspace.ts | 13 +--- .../electron-main-application.ts | 12 ++- .../electron-file-dialog-module.ts | 33 ++++++++- .../src/electron-browser/preload.ts | 3 + .../src/electron-common/electron-api.ts | 1 + 18 files changed, 298 insertions(+), 40 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 82d3a8d13745e..740eedb73db2d 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-electron-app.test.ts b/examples/playwright/src/tests/theia-electron-app.test.ts index afbdbeb36c2bb..c4a6557216665 100644 --- a/examples/playwright/src/tests/theia-electron-app.test.ts +++ b/examples/playwright/src/tests/theia-electron-app.test.ts @@ -19,38 +19,96 @@ import { TheiaExplorerView } from '../theia-explorer-view'; import { TheiaAboutDialog } from '../theia-about-dialog'; import { ElectronLaunchOptions, TheiaElectronAppLoader } from '../theia-app-loader'; import { TheiaWorkspace } from '../theia-workspace'; +import { TheiaApp } from '../theia-app'; +test.describe.configure({ mode: 'serial' }); test.describe('Theia Electron Application', () => { + let app: TheiaApp; + let ws: TheiaWorkspace; + + test.beforeAll(async () => { + ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + app = await TheiaElectronAppLoader.load(new ElectronLaunchOptions('../electron', '../../plugins'), ws); + }); + + 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 app.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 app.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 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); + }); + + 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 app.menuBar.openMenu('View')).clickMenuItem('Explorer'); + const explorerView = new TheiaExplorerView(app); + expect(await explorerView.isDisplayed()).toBe(true); + await (await app.menuBar.openMenu('View')).clickMenuItem('Explorer'); + expect(await explorerView.isDisplayed()).toBe(false); + }); }); + diff --git a/examples/playwright/src/tests/theia-output-view.test.ts b/examples/playwright/src/tests/theia-output-view.test.ts index d74c09e5badc9..2c840e7644884 100644 --- a/examples/playwright/src/tests/theia-output-view.test.ts +++ b/examples/playwright/src/tests/theia-output-view.test.ts @@ -14,20 +14,19 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 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 { TheiaBrowserAppLoader } 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 ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); }); test('should open the output view and check if is visible and active', async () => { diff --git a/examples/playwright/src/tests/theia-terminal-view.test.ts b/examples/playwright/src/tests/theia-terminal-view.test.ts index 0fa39a89d8bea..778a5cd31a1d3 100644 --- a/examples/playwright/src/tests/theia-terminal-view.test.ts +++ b/examples/playwright/src/tests/theia-terminal-view.test.ts @@ -14,19 +14,20 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 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 { 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 ({ 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); }); test('should be possible to open a new terminal', async () => { diff --git a/examples/playwright/src/tests/theia-toolbar.test.ts b/examples/playwright/src/tests/theia-toolbar.test.ts index a1a7c6b8b05ef..f10fc093b4ea8 100644 --- a/examples/playwright/src/tests/theia-toolbar.test.ts +++ b/examples/playwright/src/tests/theia-toolbar.test.ts @@ -14,18 +14,19 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 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'; let app: TheiaApp; let toolbar: TheiaToolbar; test.describe('Theia Toolbar', () => { - test.beforeAll(async () => { - app = await TheiaApp.load(page); + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + app = await TheiaBrowserAppLoader.load(page); 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 42a7e66ff548d..88f0955c54618 100644 --- a/examples/playwright/src/tests/theia-workspace.test.ts +++ b/examples/playwright/src/tests/theia-workspace.test.ts @@ -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 ({ page }) => { + const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']); + const app = await TheiaBrowserAppLoader.load(page, 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..af8fffa6eecd5 100644 --- a/examples/playwright/src/theia-app-loader.ts +++ b/examples/playwright/src/theia-app-loader.ts @@ -15,8 +15,11 @@ // ***************************************************************************** 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; @@ -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 1395fea88d35b..8a9cee5a8f762 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 ec49244cd5ad7..ab23dea239686 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -191,6 +191,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(); @@ -201,12 +202,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'; } @@ -320,6 +324,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; } @@ -327,6 +334,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 72b7ea9d14019..8a7660bae7c51 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 73536c6a41f1a..095490e5f8f6e 100644 --- a/packages/filesystem/src/electron-browser/preload.ts +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -18,13 +18,16 @@ import { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOpti // eslint-disable-next-line import/no-extraneous-dependencies 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 4441d1cb38bd7..6fe984cfd2803 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 {