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 {