From a1cae0e216bd652cd7f8921a0899c76a4ffc4e27 Mon Sep 17 00:00:00 2001 From: Nina Doschek Date: Tue, 1 Mar 2022 15:05:12 +0100 Subject: [PATCH] #10793 Fix playwright tests for Windows and MacOS - Fix workspace path encoding for Windows - Stabilize workspace path fetching - Fix explorer review title button selectors - Introduce OSUtil helper functions - Fix shortcuts for MacOS - Improve opening of TheiaViews (speeds up tests by approx. 10s) Contributed on behalf of STMicroelectronics Signed-off-by: Nina Doschek Fixes #10793 --- .../playwright/src/theia-explorer-view.ts | 17 +++++++++----- .../src/theia-quick-command-palette.ts | 4 ++-- .../playwright/src/theia-rename-dialog.ts | 4 ++-- examples/playwright/src/theia-text-editor.ts | 9 +++++--- examples/playwright/src/theia-view.ts | 1 + examples/playwright/src/theia-workspace.ts | 23 ++++++++++++------- examples/playwright/src/util.ts | 19 +++++++++++++++ .../playwright/tests/theia-main-menu.test.ts | 3 ++- 8 files changed, 58 insertions(+), 22 deletions(-) diff --git a/examples/playwright/src/theia-explorer-view.ts b/examples/playwright/src/theia-explorer-view.ts index e7178a668529d..6cc1cbba132b1 100644 --- a/examples/playwright/src/theia-explorer-view.ts +++ b/examples/playwright/src/theia-explorer-view.ts @@ -21,7 +21,7 @@ import { TheiaMenuItem } from './theia-menu-item'; import { TheiaRenameDialog } from './theia-rename-dialog'; import { TheiaTreeNode } from './theia-tree-node'; import { TheiaView } from './theia-view'; -import { elementContainsClass, normalizeId } from './util'; +import { elementContainsClass, normalizeId, OSUtil, urlEncodePath } from './util'; const TheiaExplorerViewData = { tabSelector: '#shell-tab-explorer-view-container', @@ -75,16 +75,17 @@ export class TheiaExplorerView extends TheiaView { } async refresh(): Promise { - await this.clickButton('__explorer-view-container_title:navigator.refresh'); + await this.clickButton('navigator.refresh'); } async collapseAll(): Promise { - await this.clickButton('__explorer-view-container_title:navigator.collapse.all'); + await this.clickButton('navigator.collapse.all'); } protected async clickButton(id: string): Promise { await this.activate(); const viewElement = await this.viewElement(); + await viewElement?.hover(); const button = await viewElement?.waitForSelector(`#${normalizeId(id)}`); await button?.click(); } @@ -161,7 +162,11 @@ export class TheiaExplorerView extends TheiaView { protected treeNodeId(filePath: string): string { const workspacePath = this.app.workspace.path; - return `${workspacePath}:${workspacePath}/${filePath}`; + const nodeId = `${workspacePath}:${workspacePath}${OSUtil.fileSeparator}${filePath}`; + if (OSUtil.isWindows) { + return urlEncodePath(nodeId); + } + return nodeId; } async clickContextMenuItem(file: string, path: string[]): Promise { @@ -199,8 +204,7 @@ export class TheiaExplorerView extends TheiaView { async getNumberOfVisibleNodes(): Promise { await this.activate(); - await this.app.quickCommandPalette.type('Refresh in Explorer'); - await this.app.quickCommandPalette.trigger('File: Refresh in Explorer'); + await this.refresh(); const fileStatElements = await this.visibleFileStatNodes(DOT_FILES_FILTER); return fileStatElements.length; } @@ -224,6 +228,7 @@ export class TheiaExplorerView extends TheiaView { await renameDialog.enterNewName(newName); confirm ? await renameDialog.confirm() : await renameDialog.close(); await renameDialog.waitForClosed(); + await this.refresh(); } } diff --git a/examples/playwright/src/theia-quick-command-palette.ts b/examples/playwright/src/theia-quick-command-palette.ts index ccb4b200b7053..9bf363fa50588 100644 --- a/examples/playwright/src/theia-quick-command-palette.ts +++ b/examples/playwright/src/theia-quick-command-palette.ts @@ -16,14 +16,14 @@ import { ElementHandle } from '@playwright/test'; import { TheiaPageObject } from './theia-page-object'; -import { USER_KEY_TYPING_DELAY } from './util'; +import { OSUtil, USER_KEY_TYPING_DELAY } from './util'; export class TheiaQuickCommandPalette extends TheiaPageObject { selector = '.quick-input-widget'; async open(): Promise { - await this.page.keyboard.press('Control+Shift+p'); + await this.page.keyboard.press(OSUtil.isMacOS ? 'Meta+Shift+p' : 'Control+Shift+p'); await this.page.waitForSelector(this.selector); } diff --git a/examples/playwright/src/theia-rename-dialog.ts b/examples/playwright/src/theia-rename-dialog.ts index 0f5d8261688bc..411f24de7ac48 100644 --- a/examples/playwright/src/theia-rename-dialog.ts +++ b/examples/playwright/src/theia-rename-dialog.ts @@ -15,13 +15,13 @@ // ***************************************************************************** import { TheiaDialog } from './theia-dialog'; -import { USER_KEY_TYPING_DELAY } from './util'; +import { OSUtil, USER_KEY_TYPING_DELAY } from './util'; export class TheiaRenameDialog extends TheiaDialog { async enterNewName(newName: string): Promise { const inputField = await this.page.waitForSelector(`${this.blockSelector} .theia-input`); - await inputField.press('Control+a'); + await inputField.press(OSUtil.isMacOS ? 'Meta+a' : 'Control+a'); await inputField.type(newName, { delay: USER_KEY_TYPING_DELAY }); await this.page.waitForTimeout(USER_KEY_TYPING_DELAY); } diff --git a/examples/playwright/src/theia-text-editor.ts b/examples/playwright/src/theia-text-editor.ts index 708910feacf8f..ba79021cb1b18 100644 --- a/examples/playwright/src/theia-text-editor.ts +++ b/examples/playwright/src/theia-text-editor.ts @@ -15,17 +15,20 @@ // ***************************************************************************** import { ElementHandle } from '@playwright/test'; +import { join } from 'path'; import { TheiaApp } from './theia-app'; import { TheiaEditor } from './theia-editor'; -import { normalizeId } from './util'; +import { normalizeId, OSUtil, urlEncodePath } from './util'; export class TheiaTextEditor extends TheiaEditor { constructor(filePath: string, app: TheiaApp) { + // shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1 + // code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1 super({ - tabSelector: normalizeId(`#shell-tab-code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`), - viewSelector: normalizeId(`#code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`) + '.theia-editor' + tabSelector: normalizeId(`#shell-tab-code-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}:1`), + viewSelector: normalizeId(`#code-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}:1`) + '.theia-editor' }, app); } diff --git a/examples/playwright/src/theia-view.ts b/examples/playwright/src/theia-view.ts index 10ec8acc62f4b..abbc4081fbbb9 100644 --- a/examples/playwright/src/theia-view.ts +++ b/examples/playwright/src/theia-view.ts @@ -50,6 +50,7 @@ export class TheiaView extends TheiaPageObject { if (!this.data.viewName) { throw new Error('View name must be specified to open via command palette'); } + await this.app.quickCommandPalette.type('View: Open View'); await this.app.quickCommandPalette.trigger('View: Open View...', this.data.viewName); await this.waitForVisible(); return this; diff --git a/examples/playwright/src/theia-workspace.ts b/examples/playwright/src/theia-workspace.ts index 760f265f57c55..eed7ddc2b32d1 100644 --- a/examples/playwright/src/theia-workspace.ts +++ b/examples/playwright/src/theia-workspace.ts @@ -15,9 +15,8 @@ // ***************************************************************************** import * as fs from 'fs-extra'; -import { tmpdir } from 'os'; -import { sep } from 'path'; -import * as path from 'path'; +import { resolve } from 'path'; +import { OSUtil, urlEncodePath } from './util'; export class TheiaWorkspace { @@ -30,14 +29,14 @@ 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(`${tmpdir}${sep}cloud-ws-`); + this.workspacePath = fs.mkdtempSync(`${OSUtil.tmpDir}${OSUtil.fileSeparator}cloud-ws-`); } /** Performs the file system operations preparing the workspace location synchronously. */ initialize(): void { if (this.pathOfFilesToInitialize) { for (const initPath of this.pathOfFilesToInitialize) { - const absoluteInitPath = path.resolve(process.cwd(), initPath); + const absoluteInitPath = resolve(process.cwd(), initPath); if (!fs.pathExistsSync(absoluteInitPath)) { throw Error('Workspace does not exist at ' + absoluteInitPath); } @@ -47,15 +46,23 @@ export class TheiaWorkspace { } get path(): string { - return this.workspacePath; + 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; } get urlEncodedPath(): string { - return this.path.replace(/[\\]/g, '/'); + return urlEncodePath(this.path); } get escapedPath(): string { - return this.path.replace(/:/g, '%3A').replace(/[\\]/g, '%5C'); + return this.path.replace(/:/g, '%3A'); } clear(): void { diff --git a/examples/playwright/src/util.ts b/examples/playwright/src/util.ts index 6839b5c73973e..2ccbe8c684925 100644 --- a/examples/playwright/src/util.ts +++ b/examples/playwright/src/util.ts @@ -15,6 +15,8 @@ // ***************************************************************************** import { ElementHandle } from '@playwright/test'; +import { tmpdir, platform } from 'os'; +import { sep } from 'path'; export const USER_KEY_TYPING_DELAY = 80; @@ -23,6 +25,10 @@ export function normalizeId(nodeId: string): string { return nodeId.replace(/[.:,%/\\]/g, matchedChar => '\\' + matchedChar); } +export function urlEncodePath(path: string): string { + return path.replace(/\\/g, '/'); +} + export async function toTextContentArray(items: ElementHandle[]): Promise { const contents = items.map(item => item.textContent()); const resolvedContents = await Promise.all(contents); @@ -70,3 +76,16 @@ export async function elementId(element: ElementHandle if (id === null) { throw new Error('Could not get ID of ' + element); } return id; } + +export namespace OSUtil { + export const isWindows = platform() === 'win32'; + export const isMacOS = platform() === 'darwin'; + // The platform-specific file separator '\' or '/'. + export const fileSeparator = sep; + // The platform-specific location of the temporary directory. + export const tmpDir = tmpdir(); + + export function osStartsWithFileSeparator(path: string): boolean { + return path.startsWith(fileSeparator); + } +} diff --git a/examples/playwright/tests/theia-main-menu.test.ts b/examples/playwright/tests/theia-main-menu.test.ts index b321677549966..0bbaeebaad329 100644 --- a/examples/playwright/tests/theia-main-menu.test.ts +++ b/examples/playwright/tests/theia-main-menu.test.ts @@ -15,6 +15,7 @@ // ***************************************************************************** import { expect } from '@playwright/test'; +import { OSUtil } from '../src/util'; import { TheiaApp } from '../src/theia-app'; import { TheiaMenuBar } from '../src/theia-main-menu'; import test, { page } from './fixtures/theia-fixture'; @@ -56,7 +57,7 @@ test.describe('Theia Main Menu', () => { expect(label).toBe('New File'); const shortCut = await menuItem?.shortCut(); - expect(shortCut).toBe('Alt+N'); + expect(shortCut).toBe(OSUtil.isMacOS ? 'N' : 'Alt+N'); const hasSubmenu = await menuItem?.hasSubmenu(); expect(hasSubmenu).toBe(false);