From 2ada17080cd3e351e093e5fce40d36f16db3124a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Apr 2022 10:13:08 +0200 Subject: [PATCH] Use `playwright` for desktop smoke tests (#146692) * Use `playwright` for desktop smoke tests * fix distro issues * tests - enable prefs tests for web --- .../darwin/product-build-darwin.yml | 6 +- .../linux/product-build-linux-client.yml | 6 +- .../win32/product-build-win32.yml | 6 +- src/vs/code/electron-main/app.ts | 2 +- src/vs/platform/driver/browser/baseDriver.ts | 205 ----------------- src/vs/platform/driver/browser/driver.ts | 211 +++++++++++++++++- src/vs/platform/driver/common/driver.ts | 10 +- src/vs/platform/driver/common/driverIpc.ts | 9 +- .../platform/driver/electron-main/driver.ts | 21 +- .../driver/electron-sandbox/driver.ts | 46 ++-- src/vs/platform/driver/node/driver.ts | 7 +- src/vs/platform/environment/common/argv.ts | 5 +- .../electron-main/environmentMainService.ts | 4 - src/vs/platform/environment/node/argv.ts | 2 +- src/vs/workbench/browser/window.ts | 14 +- .../electron-sandbox/desktop.main.ts | 6 - src/vs/workbench/electron-sandbox/window.ts | 24 ++ test/automation/src/application.ts | 10 +- test/automation/src/code.ts | 47 ++-- test/automation/src/driver.js | 8 +- test/automation/src/electronDriver.ts | 107 +++++---- test/automation/src/index.ts | 1 + ...htDriver.ts => playwrightBrowserDriver.ts} | 126 ++++++----- .../src/playwrightElectronDriver.ts | 61 +++++ .../tools/copy-driver-definition.js | 3 + test/automation/tools/copy-package-version.js | 3 + test/integration/browser/src/index.ts | 2 +- test/integration/electron/testrunner.js | 3 + .../src/areas/statusbar/statusbar.test.ts | 2 +- .../src/areas/workbench/data-loss.test.ts | 2 +- test/smoke/src/main.ts | 91 +++----- test/smoke/test/index.js | 18 +- test/unit/browser/index.js | 1 + test/unit/node/index.js | 1 + 34 files changed, 592 insertions(+), 478 deletions(-) delete mode 100644 src/vs/platform/driver/browser/baseDriver.ts rename test/automation/src/{playwrightDriver.ts => playwrightBrowserDriver.ts} (76%) create mode 100644 test/automation/src/playwrightElectronDriver.ts diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 6f4e8661cb2bd..1c529a95362f2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -236,7 +236,7 @@ steps: - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --headless + yarn smoketest-no-compile --web --tracing --headless timeoutInMinutes: 10 displayName: Run smoke tests (Browser, Chromium) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -245,7 +245,7 @@ steps: set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" + yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" # Increased timeout because this test downloads stable code timeoutInMinutes: 20 displayName: Run smoke tests (Electron) @@ -256,7 +256,7 @@ steps: APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote + yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" timeoutInMinutes: 10 displayName: Run smoke tests (Remote) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index 35aad2206044d..5f15732ad85cb 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -259,7 +259,7 @@ steps: - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --headless --electronArgs="--disable-dev-shm-usage" + yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" timeoutInMinutes: 10 displayName: Run smoke tests (Browser, Chromium) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -267,7 +267,7 @@ steps: - script: | set -e APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - yarn smoketest-no-compile --build "$APP_PATH" --electronArgs="--disable-dev-shm-usage" + yarn smoketest-no-compile --tracing --build "$APP_PATH" # Increased timeout because this test downloads stable code timeoutInMinutes: 20 displayName: Run smoke tests (Electron) @@ -277,7 +277,7 @@ steps: set -e APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --build "$APP_PATH" --remote --electronArgs="--disable-dev-shm-usage" + yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" timeoutInMinutes: 10 displayName: Run smoke tests (Remote) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 001ce407218c8..cd18ffe4c7ad5 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -220,7 +220,7 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --web --headless } + exec { yarn smoketest-no-compile --web --tracing --headless } displayName: Run smoke tests (Browser, Chromium) timeoutInMinutes: 10 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) @@ -229,7 +229,7 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --build "$AppRoot" } + exec { yarn smoketest-no-compile --tracing --build "$AppRoot" } displayName: Run smoke tests (Electron) # Increased timeout because this test downloads stable code timeoutInMinutes: 20 @@ -240,7 +240,7 @@ steps: $ErrorActionPreference = "Stop" $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --build "$AppRoot" --remote } + exec { yarn smoketest-no-compile --tracing --remote --build "$AppRoot" } displayName: Run smoke tests (Remote) timeoutInMinutes: 10 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d7459f90ea647..5d6b367dca09f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -524,7 +524,7 @@ export class CodeApplication extends Disposable { // Create driver if (this.environmentMainService.driverHandle) { - const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, this.environmentMainService, appInstantiationService); + const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, appInstantiationService); this.logService.info('Driver started at:', this.environmentMainService.driverHandle); this._register(server); diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts deleted file mode 100644 index 9875672768af9..0000000000000 --- a/src/vs/platform/driver/browser/baseDriver.ts +++ /dev/null @@ -1,205 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom'; -import { coalesce } from 'vs/base/common/arrays'; -import { language, locale } from 'vs/base/common/platform'; -import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver'; -import localizedStrings from 'vs/platform/localizations/common/localizedStrings'; - -export abstract class BaseWindowDriver implements IWindowDriver { - - abstract click(selector: string, xoffset?: number, yoffset?: number): Promise; - abstract doubleClick(selector: string): Promise; - - async setValue(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const inputElement = element as HTMLInputElement; - inputElement.value = text; - - const event = new Event('input', { bubbles: true, cancelable: true }); - inputElement.dispatchEvent(event); - } - - async getTitle(): Promise { - return document.title; - } - - async isActiveElement(selector: string): Promise { - const element = document.querySelector(selector); - - if (element !== document.activeElement) { - const chain: string[] = []; - let el = document.activeElement; - - while (el) { - const tagName = el.tagName; - const id = el.id ? `#${el.id}` : ''; - const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); - chain.unshift(`${tagName}${id}${classes}`); - - el = el.parentElement; - } - - throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); - } - - return true; - } - - async getElements(selector: string, recursive: boolean): Promise { - const query = document.querySelectorAll(selector); - const result: IElement[] = []; - for (let i = 0; i < query.length; i++) { - const element = query.item(i); - result.push(this.serializeElement(element, recursive)); - } - - return result; - } - - private serializeElement(element: Element, recursive: boolean): IElement { - const attributes = Object.create(null); - - for (let j = 0; j < element.attributes.length; j++) { - const attr = element.attributes.item(j); - if (attr) { - attributes[attr.name] = attr.value; - } - } - - const children: IElement[] = []; - - if (recursive) { - for (let i = 0; i < element.children.length; i++) { - const child = element.children.item(i); - if (child) { - children.push(this.serializeElement(child, true)); - } - } - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - - return { - tagName: element.tagName, - className: element.className, - textContent: element.textContent || '', - attributes, - children, - left, - top - }; - } - - async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - return this._getElementXY(selector, offset); - } - - async typeInEditor(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Editor not found: ${selector}`); - } - - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); - } - - async getTerminalBuffer(selector: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Terminal not found: ${selector}`); - } - - const xterm = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - const lines: string[] = []; - for (let i = 0; i < xterm.buffer.active.length; i++) { - lines.push(xterm.buffer.active.getLine(i)!.translateToString(true)); - } - - return lines; - } - - async writeInTerminal(selector: string, text: string): Promise { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Element not found: ${selector}`); - } - - const xterm = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - xterm._core.coreService.triggerDataEvent(text); - } - - getLocaleInfo(): Promise { - return Promise.resolve({ - language: language, - locale: locale - }); - } - - getLocalizedStrings(): Promise { - return Promise.resolve({ - open: localizedStrings.open, - close: localizedStrings.close, - find: localizedStrings.find - }); - } - - protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - const { width, height } = getClientArea(element as HTMLElement); - let x: number, y: number; - - if (offset) { - x = left + offset.x; - y = top + offset.y; - } else { - x = left + (width / 2); - y = top + (height / 2); - } - - x = Math.round(x); - y = Math.round(y); - - return { x, y }; - } - - abstract openDevTools(): Promise; -} diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts index 7027a0d37b6db..c57855c3828c5 100644 --- a/src/vs/platform/driver/browser/driver.ts +++ b/src/vs/platform/driver/browser/driver.ts @@ -3,23 +3,210 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; +import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom'; +import { coalesce } from 'vs/base/common/arrays'; +import { language, locale } from 'vs/base/common/platform'; +import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver'; +import localizedStrings from 'vs/platform/localizations/common/localizedStrings'; -class BrowserWindowDriver extends BaseWindowDriver { - click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise { - throw new Error('Method not implemented.'); +export class BrowserWindowDriver implements IWindowDriver { + + async setValue(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const inputElement = element as HTMLInputElement; + inputElement.value = text; + + const event = new Event('input', { bubbles: true, cancelable: true }); + inputElement.dispatchEvent(event); } - doubleClick(selector: string): Promise { - throw new Error('Method not implemented.'); + + async getTitle(): Promise { + return document.title; + } + + async isActiveElement(selector: string): Promise { + const element = document.querySelector(selector); + + if (element !== document.activeElement) { + const chain: string[] = []; + let el = document.activeElement; + + while (el) { + const tagName = el.tagName; + const id = el.id ? `#${el.id}` : ''; + const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); + chain.unshift(`${tagName}${id}${classes}`); + + el = el.parentElement; + } + + throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); + } + + return true; + } + + async getElements(selector: string, recursive: boolean): Promise { + const query = document.querySelectorAll(selector); + const result: IElement[] = []; + for (let i = 0; i < query.length; i++) { + const element = query.item(i); + result.push(this.serializeElement(element, recursive)); + } + + return result; + } + + private serializeElement(element: Element, recursive: boolean): IElement { + const attributes = Object.create(null); + + for (let j = 0; j < element.attributes.length; j++) { + const attr = element.attributes.item(j); + if (attr) { + attributes[attr.name] = attr.value; + } + } + + const children: IElement[] = []; + + if (recursive) { + for (let i = 0; i < element.children.length; i++) { + const child = element.children.item(i); + if (child) { + children.push(this.serializeElement(child, true)); + } + } + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + + return { + tagName: element.tagName, + className: element.className, + textContent: element.textContent || '', + attributes, + children, + left, + top + }; + } + + async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { + const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; + return this._getElementXY(selector, offset); + } + + async typeInEditor(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Editor not found: ${selector}`); + } + + const textarea = element as HTMLTextAreaElement; + const start = textarea.selectionStart; + const newStart = start + text.length; + const value = textarea.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + textarea.value = newValue; + textarea.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + textarea.dispatchEvent(event); + } + + async getTerminalBuffer(selector: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Terminal not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + const lines: string[] = []; + for (let i = 0; i < xterm.buffer.active.length; i++) { + lines.push(xterm.buffer.active.getLine(i)!.translateToString(true)); + } + + return lines; + } + + async writeInTerminal(selector: string, text: string): Promise { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Element not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + xterm._core.coreService.triggerDataEvent(text); } - openDevTools(): Promise { + + getLocaleInfo(): Promise { + return Promise.resolve({ + language: language, + locale: locale + }); + } + + getLocalizedStrings(): Promise { + return Promise.resolve({ + open: localizedStrings.open, + close: localizedStrings.close, + find: localizedStrings.find + }); + } + + protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + const { width, height } = getClientArea(element as HTMLElement); + let x: number, y: number; + + if (offset) { + x = left + offset.x; + y = top + offset.y; + } else { + x = left + (width / 2); + y = top + (height / 2); + } + + x = Math.round(x); + y = Math.round(y); + + return { x, y }; + } + + click(selector: string, xoffset?: number, yoffset?: number): Promise { + + // This is actually not used in the playwright drivers + // that can implement `click` natively via the driver + throw new Error('Method not implemented.'); } } -export async function registerWindowDriver(): Promise { - (window).driver = new BrowserWindowDriver(); - - return Disposable.None; +export function registerWindowDriver(): void { + Object.assign(window, { driver: new BrowserWindowDriver() }); } diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index ca36bc4409b2c..6d23ce9748ce5 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -44,10 +44,9 @@ export interface IDriver { startTracing(windowId: number, name: string): Promise; stopTracing(windowId: number, name: string, persist: boolean): Promise; reloadWindow(windowId: number): Promise; - exitApplication(): Promise; + exitApplication(): Promise; dispatchKeybinding(windowId: number, keybinding: string): Promise; click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(windowId: number, selector: string): Promise; setValue(windowId: number, selector: string, text: string): Promise; getTitle(windowId: number): Promise; isActiveElement(windowId: number, selector: string): Promise; @@ -62,7 +61,6 @@ export interface IDriver { export interface IWindowDriver { click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(selector: string): Promise; setValue(selector: string, text: string): Promise; getTitle(): Promise; isActiveElement(selector: string): Promise; @@ -79,11 +77,7 @@ export interface IWindowDriver { export const ID = 'driverService'; export const IDriver = createDecorator(ID); -export interface IDriverOptions { - verbose: boolean; -} - export interface IWindowDriverRegistry { - registerWindowDriver(windowId: number): Promise; + registerWindowDriver(windowId: number): Promise; reloadWindowDriver(windowId: number): Promise; } diff --git a/src/vs/platform/driver/common/driverIpc.ts b/src/vs/platform/driver/common/driverIpc.ts index 49593c7588264..6d5ed5e55c417 100644 --- a/src/vs/platform/driver/common/driverIpc.ts +++ b/src/vs/platform/driver/common/driverIpc.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; +import { IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; export class WindowDriverChannel implements IServerChannel { @@ -18,7 +18,6 @@ export class WindowDriverChannel implements IServerChannel { call(_: unknown, command: string, arg?: any): Promise { switch (command) { case 'click': return this.driver.click(arg[0], arg[1], arg[2]); - case 'doubleClick': return this.driver.doubleClick(arg); case 'setValue': return this.driver.setValue(arg[0], arg[1]); case 'getTitle': return this.driver.getTitle(); case 'isActiveElement': return this.driver.isActiveElement(arg); @@ -45,10 +44,6 @@ export class WindowDriverChannelClient implements IWindowDriver { return this.channel.call('click', [selector, xoffset, yoffset]); } - doubleClick(selector: string): Promise { - return this.channel.call('doubleClick', selector); - } - setValue(selector: string, text: string): Promise { return this.channel.call('setValue', [selector, text]); } @@ -96,7 +91,7 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry constructor(private channel: IChannel) { } - registerWindowDriver(windowId: number): Promise { + registerWindowDriver(windowId: number): Promise { return this.channel.call('registerWindowDriver', windowId); } diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index aa1e5f6ee79fc..f14f14e40ad95 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -12,7 +12,7 @@ import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; +import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc'; import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -40,7 +40,6 @@ export class Driver implements IDriver, IWindowDriverRegistry { constructor( private windowServer: IPCServer, - private options: IDriverOptions, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IFileService private readonly fileService: IFileService, @@ -48,13 +47,12 @@ export class Driver implements IDriver, IWindowDriverRegistry { @ILogService private readonly logService: ILogService ) { } - async registerWindowDriver(windowId: number): Promise { + async registerWindowDriver(windowId: number): Promise { this.logService.info(`[driver] registerWindowDriver(${windowId})`); this.registeredWindowIds.add(windowId); this.reloadingWindowIds.delete(windowId); this.onDidReloadingChange.fire(); - return this.options; } async reloadWindowDriver(windowId: number): Promise { @@ -111,10 +109,12 @@ export class Driver implements IDriver, IWindowDriverRegistry { this.lifecycleMainService.reload(window); } - exitApplication(): Promise { + async exitApplication(): Promise { this.logService.info(`[driver] exitApplication()`); - return this.lifecycleMainService.quit(); + this.lifecycleMainService.quit(); + + return process.pid; } async dispatchKeybinding(windowId: number, keybinding: string): Promise { @@ -175,11 +175,6 @@ export class Driver implements IDriver, IWindowDriverRegistry { await windowDriver.click(selector, xoffset, yoffset); } - async doubleClick(windowId: number, selector: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.doubleClick(selector); - } - async setValue(windowId: number, selector: string, text: string): Promise { const windowDriver = await this.getWindowDriver(windowId); await windowDriver.setValue(selector, text); @@ -249,11 +244,9 @@ export class Driver implements IDriver, IWindowDriverRegistry { export async function serve( windowServer: IPCServer, handle: string, - environmentMainService: IEnvironmentMainService, instantiationService: IInstantiationService ): Promise { - const verbose = environmentMainService.driverVerbose; - const driver = instantiationService.createInstance(Driver, windowServer, { verbose }); + const driver = instantiationService.createInstance(Driver, windowServer); const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel); diff --git a/src/vs/platform/driver/electron-sandbox/driver.ts b/src/vs/platform/driver/electron-sandbox/driver.ts index 84bc965bd9bbd..bc2e4c9ae38fe 100644 --- a/src/vs/platform/driver/electron-sandbox/driver.ts +++ b/src/vs/platform/driver/electron-sandbox/driver.ts @@ -5,13 +5,32 @@ import { timeout } from 'vs/base/common/async'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; +import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver'; import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -class WindowDriver extends BaseWindowDriver { +interface INativeWindowDriverHelper { + exitApplication(): Promise; +} + +class NativeWindowDriver extends BrowserWindowDriver { + + constructor(private readonly helper: INativeWindowDriverHelper) { + super(); + } + + exitApplication(): Promise { + return this.helper.exitApplication(); + } +} + +export function registerWindowDriver(helper: INativeWindowDriverHelper): void { + Object.assign(window, { driver: new NativeWindowDriver(helper) }); +} + +class LegacyNativeWindowDriver extends BrowserWindowDriver { constructor( @INativeHostService private readonly nativeHostService: INativeHostService @@ -19,16 +38,13 @@ class WindowDriver extends BaseWindowDriver { super(); } - click(selector: string, xoffset?: number, yoffset?: number): Promise { + override click(selector: string, xoffset?: number, yoffset?: number): Promise { const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - return this._click(selector, 1, offset); - } - doubleClick(selector: string): Promise { - return this._click(selector, 2); + return this.doClick(selector, 1, offset); } - private async _click(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise { + private async doClick(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise { const { x, y } = await this._getElementXY(selector, offset); await this.nativeHostService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); @@ -37,17 +53,19 @@ class WindowDriver extends BaseWindowDriver { await this.nativeHostService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); await timeout(100); } - - async openDevTools(): Promise { - await this.nativeHostService.openDevTools({ mode: 'detach' }); - } } -export async function registerWindowDriver(accessor: ServicesAccessor, windowId: number): Promise { +/** + * Old school window driver that is implemented by us + * from the main process. + * + * @deprecated + */ +export async function registerLegacyWindowDriver(accessor: ServicesAccessor, windowId: number): Promise { const instantiationService = accessor.get(IInstantiationService); const mainProcessService = accessor.get(IMainProcessService); - const windowDriver = instantiationService.createInstance(WindowDriver); + const windowDriver = instantiationService.createInstance(LegacyNativeWindowDriver); const windowDriverChannel = new WindowDriverChannel(windowDriver); mainProcessService.registerChannel('windowDriver', windowDriverChannel); diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index 4e56bc540e4b1..29abf734bec5f 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -27,7 +27,6 @@ export class DriverChannel implements IServerChannel { case 'exitApplication': return this.driver.exitApplication(); case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); - case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]); case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); case 'getTitle': return this.driver.getTitle(arg[0]); case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); @@ -70,7 +69,7 @@ export class DriverChannelClient implements IDriver { return this.channel.call('reloadWindow', windowId); } - exitApplication(): Promise { + exitApplication(): Promise { return this.channel.call('exitApplication'); } @@ -82,10 +81,6 @@ export class DriverChannelClient implements IDriver { return this.channel.call('click', [windowId, selector, xoffset, yoffset]); } - doubleClick(windowId: number, selector: string): Promise { - return this.channel.call('doubleClick', [windowId, selector]); - } - setValue(windowId: number, selector: string, text: string): Promise { return this.channel.call('setValue', [windowId, selector, text]); } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 4815b4a288853..ccac10545b5da 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -79,8 +79,11 @@ export interface NativeParsedArgs { 'max-memory'?: string; 'file-write'?: boolean; 'file-chmod'?: boolean; + /** + * @deprecated use `enable-smoke-test-driver` + */ 'driver'?: string; - 'driver-verbose'?: boolean; + 'enable-smoke-test-driver'?: boolean; 'remote'?: string; 'force'?: boolean; 'do-not-sync'?: boolean; diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index beb1896fa2b3f..6834b8115fe19 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -35,7 +35,6 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { // --- config sandbox: boolean; - driverVerbose: boolean; disableUpdates: boolean; } @@ -59,9 +58,6 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get sandbox(): boolean { return !!this.args['__sandbox']; } - @memoize - get driverVerbose(): boolean { return !!this.args['driver-verbose']; } - @memoize get disableUpdates(): boolean { return !!this.args['disable-updates']; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index c671917d1a459..1d7aadc1f1474 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -99,6 +99,7 @@ export const OPTIONS: OptionDescriptions> = { 'export-default-configuration': { type: 'string' }, 'install-source': { type: 'string' }, 'driver': { type: 'string' }, + 'enable-smoke-test-driver': { type: 'boolean' }, 'logExtensionHostCommunication': { type: 'boolean' }, 'skip-release-notes': { type: 'boolean' }, 'skip-welcome': { type: 'boolean' }, @@ -114,7 +115,6 @@ export const OPTIONS: OptionDescriptions> = { 'open-url': { type: 'boolean' }, 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, - 'driver-verbose': { type: 'boolean' }, 'install-builtin-extension': { type: 'string[]' }, 'force': { type: 'boolean' }, 'do-not-sync': { type: 'boolean' }, diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index cbcbac8602cf2..5e94ea2bba129 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -112,16 +112,20 @@ export class BrowserWindow extends Disposable { private create(): void { - // Driver - if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { - (async () => this._register(await registerWindowDriver()))(); - } - // Handle open calls this.setupOpenHandlers(); // Label formatting this.registerLabelFormatters(); + + // Smoke Test Driver + this.setupDriver(); + } + + private setupDriver(): void { + if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { + registerWindowDriver(); + } } private setupOpenHandlers(): void { diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index e4c63f35ce97b..6a78e65d27c07 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -43,7 +43,6 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { NativeLogService } from 'vs/workbench/services/log/electron-sandbox/logService'; import { WorkspaceTrustEnablementService, WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; import { safeStringify } from 'vs/base/common/objects'; import { ISharedProcessWorkerWorkbenchService, SharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; import { isCI, isMacintosh } from 'vs/base/common/platform'; @@ -115,11 +114,6 @@ export class DesktopMain extends Disposable { // Window this._register(instantiationService.createInstance(NativeWindow)); - - // Driver - if (this.configuration.driver) { - instantiationService.invokeFunction(async accessor => this._register(await registerWindowDriver(accessor, this.configuration.windowId))); - } } private getExtraClasses(): string[] { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 816fe88f29eb7..389c1218e6740 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -63,6 +63,7 @@ import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { registerLegacyWindowDriver, registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; export class NativeWindow extends Disposable { @@ -570,6 +571,29 @@ export class NativeWindow extends Disposable { this.nativeHostService.openDevTools(); } } + + // Smoke Test Driver + this.setupDriver(); + } + + private setupDriver(): void { + + // Browser Driver + if (this.environmentService.args['enable-smoke-test-driver']) { + const that = this; + registerWindowDriver({ + async exitApplication(): Promise { + that.nativeHostService.quit(); + + return that.environmentService.mainPid; + } + }); + } + + // Legacy Driver (TODO@bpasero remove me eventually) + else if (this.environmentService.args.driver) { + this.instantiationService.invokeFunction(async accessor => this._register(await registerLegacyWindowDriver(accessor, this.nativeHostService.windowId))); + } } private setupOpenHandlers(): void { diff --git a/test/automation/src/application.ts b/test/automation/src/application.ts index 15b7fbd9a9241..2a6df7dc49bdc 100644 --- a/test/automation/src/application.ts +++ b/test/automation/src/application.ts @@ -52,6 +52,10 @@ export class Application { return !!this.options.web; } + get legacy(): boolean { + return !!this.options.legacy; + } + private _workspacePathOrFolder: string; get workspacePathOrFolder(): string { return this._workspacePathOrFolder; @@ -115,11 +119,11 @@ export class Application { } private async takeScreenshot(name: string): Promise { - if (this.web) { - return; // supported only on desktop + if (this.web || !this.legacy) { + return; // supported only on desktop (legacy) } - // Desktop: call `stopTracing` to take a screenshot + // Desktop (legacy): call `stopTracing` to take a screenshot return this._code?.stopTracing(name, true); } diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index d728c2dfddb3b..33eba97ba52f6 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; +import { join } from 'path'; import * as os from 'os'; import * as cp from 'child_process'; import { IDriver, IDisposable, IElement, Thenable, ILocalizedStrings, ILocaleInfo } from './driver'; import { launch as launchElectron } from './electronDriver'; -import { launch as launchPlaywright } from './playwrightDriver'; +import { launch as launchPlaywrightBrowser } from './playwrightBrowserDriver'; +import { launch as launchPlaywrightElectron } from './playwrightElectronDriver'; import { Logger, measureAndLog } from './logger'; import { copyExtension } from './extensions'; import * as treekill from 'tree-kill'; -const repoPath = path.join(__dirname, '../../..'); +const rootPath = join(__dirname, '../../..'); export interface LaunchOptions { codePath?: string; @@ -21,10 +22,13 @@ export interface LaunchOptions { userDataDir: string; extensionsPath: string; logger: Logger; + logsPath: string; verbose?: boolean; extraArgs?: string[]; remote?: boolean; web?: boolean; + legacy?: boolean; + tracing?: boolean; headless?: boolean; browser?: 'chromium' | 'webkit' | 'firefox'; } @@ -71,22 +75,29 @@ export async function launch(options: LaunchOptions): Promise { throw new Error('Smoke test process has terminated, refusing to spawn Code'); } - await measureAndLog(copyExtension(repoPath, options.extensionsPath, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', options.logger); + await measureAndLog(copyExtension(rootPath, options.extensionsPath, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', options.logger); // Browser smoke tests if (options.web) { - const { serverProcess, client, driver, kill } = await measureAndLog(launchPlaywright(options), 'launch playwright', options.logger); + const { serverProcess, client, driver, kill } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server', kill); - return new Code(client, driver, options.logger, serverProcess); + return new Code(client, driver, options.logger); } - // Electron smoke tests + // Electron smoke tests (playwright) + else if (!options.legacy) { + const { client, driver } = await measureAndLog(launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); + + return new Code(client, driver, options.logger); + } + + // Electron smoke tests (legacy driver) else { const { electronProcess, client, driver, kill } = await measureAndLog(launchElectron(options), 'launch electron', options.logger); registerInstance(electronProcess, options.logger, 'electron', kill); - return new Code(client, driver, options.logger, electronProcess); + return new Code(client, driver, options.logger); } } @@ -134,8 +145,7 @@ export class Code { constructor( private client: IDisposable, driver: IDriver, - readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + readonly logger: Logger ) { this.driver = new Proxy(driver, { get(target, prop) { @@ -185,18 +195,13 @@ export class Code { } async exit(): Promise { + + // Start the exit flow via driver + const pid = await measureAndLog(this.driver.exitApplication(), 'driver.exitApplication()', this.logger); + return measureAndLog(new Promise((resolve, reject) => { let done = false; - // Start the exit flow via driver - this.driver.exitApplication().then(veto => { - if (veto) { - done = true; - reject(new Error('Smoke test exit call resulted in unexpected veto')); - } - }); - - // Await the exit of the application (async () => { let retries = 0; while (!done) { @@ -206,7 +211,7 @@ export class Code { this.logger.log('Smoke test exit call did not terminate process after 10s, forcefully exiting the application...'); // no need to await since we're polling for the process to die anyways - treekill(this.mainProcess.pid!, err => { + treekill(pid, err => { this.logger.log('Failed to kill Electron process tree:', err?.message); }); } @@ -217,7 +222,7 @@ export class Code { } try { - process.kill(this.mainProcess.pid!, 0); // throws an exception if the process doesn't exist anymore. + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { done = true; diff --git a/test/automation/src/driver.js b/test/automation/src/driver.js index 1da40e42b067d..c415029cdf9d8 100644 --- a/test/automation/src/driver.js +++ b/test/automation/src/driver.js @@ -3,10 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check +'use strict'; + const path = require('path'); exports.connect = function (outPath, handle) { const bootstrapPath = path.join(outPath, 'bootstrap-amd.js'); const { load } = require(bootstrapPath); - return new Promise((c, e) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e)); -}; \ No newline at end of file + + return new Promise((resolve, reject) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(resolve, reject), reject)); +}; diff --git a/test/automation/src/electronDriver.ts b/test/automation/src/electronDriver.ts index ed6b77261334c..e9574a9e725d9 100644 --- a/test/automation/src/electronDriver.ts +++ b/test/automation/src/electronDriver.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as os from 'os'; +import { join } from 'path'; +import { platform } from 'os'; import { tmpName } from 'tmp'; import { connect as connectElectronDriver, IDisposable, IDriver } from './driver'; import { ChildProcess, spawn, SpawnOptions } from 'child_process'; @@ -16,15 +16,17 @@ import { URI } from 'vscode-uri'; import { Logger, measureAndLog } from './logger'; import type { LaunchOptions } from './code'; -const repoPath = path.join(__dirname, '../../..'); +const root = join(__dirname, '..', '..', '..'); -export async function launch(options: LaunchOptions): Promise<{ electronProcess: ChildProcess; client: IDisposable; driver: IDriver; kill: () => Promise }> { - const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, verbose, extraArgs } = options; - const env = { ...process.env }; - const logsPath = path.join(repoPath, '.build', 'logs', remote ? 'smoke-tests-remote' : 'smoke-tests'); - const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); +export interface IElectronConfiguration { + readonly electronPath: string; + readonly args: string[]; + readonly env?: NodeJS.ProcessEnv; +} - const driverIPCHandle = await measureAndLog(createDriverHandle(), 'createDriverHandle', logger); +export async function resolveElectronConfiguration(options: LaunchOptions): Promise { + const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, logsPath, extraArgs } = options; + const env = { ...process.env }; const args = [ workspacePath, @@ -38,8 +40,7 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: '--disable-workspace-trust', `--extensions-dir=${extensionsPath}`, `--user-data-dir=${userDataDir}`, - `--logsPath=${logsPath}`, - '--driver', driverIPCHandle + `--logsPath=${logsPath}` ]; if (process.platform === 'linux') { @@ -52,7 +53,7 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: if (codePath) { // running against a build: copy the test resolver extension - await measureAndLog(copyExtension(repoPath, extensionsPath, 'vscode-test-resolver'), 'copyExtension(vscode-test-resolver)', logger); + await measureAndLog(copyExtension(root, extensionsPath, 'vscode-test-resolver'), 'copyExtension(vscode-test-resolver)', logger); } args.push('--enable-proposed-api=vscode.vscode-test-resolver'); const remoteDataDir = `${userDataDir}-server`; @@ -60,26 +61,19 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: if (codePath) { // running against a build: copy the test resolver extension into remote extensions dir - const remoteExtensionsDir = path.join(remoteDataDir, 'extensions'); + const remoteExtensionsDir = join(remoteDataDir, 'extensions'); mkdirp.sync(remoteExtensionsDir); - await measureAndLog(copyExtension(repoPath, remoteExtensionsDir, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', logger); + await measureAndLog(copyExtension(root, remoteExtensionsDir, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', logger); } env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; - env['TESTRESOLVER_LOGS_FOLDER'] = path.join(logsPath, 'server'); + env['TESTRESOLVER_LOGS_FOLDER'] = join(logsPath, 'server'); } - const spawnOptions: SpawnOptions = { env }; - args.push('--enable-proposed-api=vscode.vscode-notebook-tests'); if (!codePath) { - args.unshift(repoPath); - } - - if (verbose) { - args.push('--driver-verbose'); - spawnOptions.stdio = ['ignore', 'inherit', 'inherit']; + args.unshift(root); } if (extraArgs) { @@ -87,6 +81,32 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: } const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); + + return { + env, + args, + electronPath + }; +} + +/** + * @deprecated should use the playwright based electron support instead + */ +export async function launch(options: LaunchOptions): Promise<{ electronProcess: ChildProcess; client: IDisposable; driver: IDriver; kill: () => Promise }> { + const { codePath, logger, verbose } = options; + const { env, args, electronPath } = await resolveElectronConfiguration(options); + + const driverIPCHandle = await measureAndLog(createDriverHandle(), 'createDriverHandle', logger); + args.push('--driver', driverIPCHandle); + + const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); + + const spawnOptions: SpawnOptions = { env }; + + if (verbose) { + spawnOptions.stdio = ['ignore', 'inherit', 'inherit']; + } + const electronProcess = spawn(electronPath, args, spawnOptions); logger.log(`Started electron for desktop smoke tests on pid ${electronProcess.pid}`); @@ -150,56 +170,65 @@ async function teardown(electronProcess: ChildProcess, logger: Logger): Promise< logger.log(`Gave up tearing down electron client after ${retries} attempts...`); } -function getDevElectronPath(): string { - const buildPath = path.join(repoPath, '.build'); - const product = require(path.join(repoPath, 'product.json')); +export function getDevElectronPath(): string { + const buildPath = join(root, '.build'); + const product = require(join(root, 'product.json')); switch (process.platform) { case 'darwin': - return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron'); + return join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron'); case 'linux': - return path.join(buildPath, 'electron', `${product.applicationName}`); + return join(buildPath, 'electron', `${product.applicationName}`); case 'win32': - return path.join(buildPath, 'electron', `${product.nameShort}.exe`); + return join(buildPath, 'electron', `${product.nameShort}.exe`); default: throw new Error('Unsupported platform.'); } } -function getBuildElectronPath(root: string): string { +export function getBuildElectronPath(root: string): string { switch (process.platform) { case 'darwin': - return path.join(root, 'Contents', 'MacOS', 'Electron'); + return join(root, 'Contents', 'MacOS', 'Electron'); case 'linux': { - const product = require(path.join(root, 'resources', 'app', 'product.json')); - return path.join(root, product.applicationName); + const product = require(join(root, 'resources', 'app', 'product.json')); + return join(root, product.applicationName); } case 'win32': { - const product = require(path.join(root, 'resources', 'app', 'product.json')); - return path.join(root, `${product.nameShort}.exe`); + const product = require(join(root, 'resources', 'app', 'product.json')); + return join(root, `${product.nameShort}.exe`); } default: throw new Error('Unsupported platform.'); } } +export function getBuildVersion(root: string): string { + switch (process.platform) { + case 'darwin': + return require(join(root, 'Contents', 'Resources', 'app', 'package.json')).version; + default: + return require(join(root, 'resources', 'app', 'package.json')).version; + } +} + function getDevOutPath(): string { - return path.join(repoPath, 'out'); + return join(root, 'out'); } function getBuildOutPath(root: string): string { switch (process.platform) { case 'darwin': - return path.join(root, 'Contents', 'Resources', 'app', 'out'); + return join(root, 'Contents', 'Resources', 'app', 'out'); default: - return path.join(root, 'resources', 'app', 'out'); + return join(root, 'resources', 'app', 'out'); } } async function createDriverHandle(): Promise { // Windows - if ('win32' === os.platform()) { + if ('win32' === platform()) { const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join(''); return `\\\\.\\pipe\\${name}`; } diff --git a/test/automation/src/index.ts b/test/automation/src/index.ts index 61f72fe732285..884444f58f50b 100644 --- a/test/automation/src/index.ts +++ b/test/automation/src/index.ts @@ -26,3 +26,4 @@ export * from './viewlet'; export * from './localization'; export * from './workbench'; export * from './driver'; +export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electronDriver'; diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightBrowserDriver.ts similarity index 76% rename from test/automation/src/playwrightDriver.ts rename to test/automation/src/playwrightBrowserDriver.ts index 4f0f7a94fba5a..8af107f068183 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightBrowserDriver.ts @@ -15,38 +15,32 @@ import { PageFunction } from 'playwright-core/types/structs'; import { Logger, measureAndLog } from './logger'; import type { LaunchOptions } from './code'; -const width = 1200; -const height = 800; - -const root = join(__dirname, '..', '..', '..'); -const logsPath = join(root, '.build', 'logs', 'smoke-tests-browser'); - -const vscodeToPlaywrightKey: { [key: string]: string } = { - cmd: 'Meta', - ctrl: 'Control', - shift: 'Shift', - enter: 'Enter', - escape: 'Escape', - right: 'ArrowRight', - up: 'ArrowUp', - down: 'ArrowDown', - left: 'ArrowLeft', - home: 'Home', - esc: 'Escape' -}; - -let traceCounter = 1; - -class PlaywrightDriver implements IDriver { +export class PlaywrightDriver implements IDriver { + + private static traceCounter = 1; + + private static readonly vscodeToPlaywrightKey: { [key: string]: string } = { + cmd: 'Meta', + ctrl: 'Control', + shift: 'Shift', + enter: 'Enter', + escape: 'Escape', + right: 'ArrowRight', + up: 'ArrowUp', + down: 'ArrowDown', + left: 'ArrowLeft', + home: 'Home', + esc: 'Escape' + }; _serviceBrand: undefined; constructor( - private readonly server: ChildProcess, - private readonly browser: playwright.Browser, + private readonly application: playwright.Browser | playwright.ElectronApplication, private readonly context: playwright.BrowserContext, private readonly page: playwright.Page, - private readonly logger: Logger + private readonly serverPid: number | undefined, + private readonly options: LaunchOptions ) { } @@ -59,46 +53,72 @@ class PlaywrightDriver implements IDriver { } async startTracing(windowId: number, name: string): Promise { + if (!this.options.tracing) { + return; // tracing disabled + } + try { - await measureAndLog(this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.logger); + await measureAndLog(this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.options.logger); } catch (error) { // Ignore } } async stopTracing(windowId: number, name: string, persist: boolean): Promise { + if (!this.options.tracing) { + return; // tracing disabled + } + try { let persistPath: string | undefined = undefined; if (persist) { - persistPath = join(logsPath, `playwright-trace-${traceCounter++}-${name.replace(/\s+/g, '-')}.zip`); + persistPath = join(this.options.logsPath, `playwright-trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`); } - await measureAndLog(this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.logger); + await measureAndLog(this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.options.logger); } catch (error) { // Ignore } } async reloadWindow(windowId: number) { - throw new Error('Unsupported'); + await this.page.reload(); } async exitApplication() { + + // Stop tracing try { - await measureAndLog(this.context.tracing.stop(), 'stop tracing', this.logger); + if (this.options.tracing) { + await measureAndLog(this.context.tracing.stop(), 'stop tracing', this.options.logger); + } } catch (error) { // Ignore } + // VSCode shutdown (desktop only) + let mainPid: number | undefined = undefined; + if (!this.options.web) { + try { + mainPid = await measureAndLog(this._evaluateWithDriver(([driver]) => (driver as unknown as IDriver).exitApplication()), 'driver.exitApplication()', this.options.logger); + } catch (error) { + this.options.logger.log(`Error exiting appliction (${error})`); + } + } + + // Playwright shutdown try { - await measureAndLog(this.browser.close(), 'Browser.close()', this.logger); + await measureAndLog(this.application.close(), 'playwright.close()', this.options.logger); } catch (error) { - // Ignore + this.options.logger.log(`Error closing appliction (${error})`); } - await measureAndLog(teardown(this.server, this.logger), 'teardown server', this.logger); + // Server shutdown + if (typeof this.serverPid === 'number') { + await measureAndLog(teardown(this.serverPid, this.options.logger), 'teardown server', this.options.logger); + } - return false; + return mainPid ?? this.serverPid! /* when running web we must have a server Pid */; } async dispatchKeybinding(windowId: number, keybinding: string) { @@ -117,8 +137,8 @@ class PlaywrightDriver implements IDriver { const keys = chord.split('+'); const keysDown: string[] = []; for (let i = 0; i < keys.length; i++) { - if (keys[i] in vscodeToPlaywrightKey) { - keys[i] = vscodeToPlaywrightKey[keys[i]]; + if (keys[i] in PlaywrightDriver.vscodeToPlaywrightKey) { + keys[i] = PlaywrightDriver.vscodeToPlaywrightKey[keys[i]]; } await this.page.keyboard.down(keys[i]); keysDown.push(keys[i]); @@ -136,10 +156,6 @@ class PlaywrightDriver implements IDriver { await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0)); } - async doubleClick(windowId: number, selector: string) { - throw new Error('Unsupported'); - } - async setValue(windowId: number, selector: string, text: string) { return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this._getDriverHandle(), selector, text] as const); } @@ -188,12 +204,13 @@ class PlaywrightDriver implements IDriver { return new Promise(resolve => setTimeout(resolve, ms)); } - // TODO: Cache private async _getDriverHandle(): Promise> { return this.page.evaluateHandle('window.driver'); } } +const root = join(__dirname, '..', '..', '..'); + let port = 9000; export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; client: IDisposable; driver: IDriver; kill: () => Promise }> { @@ -209,13 +226,13 @@ export async function launch(options: LaunchOptions): Promise<{ serverProcess: C client: { dispose: () => { /* there is no client to dispose for browser, teardown is triggered via exitApplication call */ } }, - driver: new PlaywrightDriver(serverProcess, browser, context, page, options.logger), - kill: () => teardown(serverProcess, options.logger) + driver: new PlaywrightDriver(browser, context, page, serverProcess.pid, options), + kill: () => teardown(serverProcess.pid, options.logger) }; } async function launchServer(options: LaunchOptions) { - const { userDataDir, codePath, extensionsPath, logger } = options; + const { userDataDir, codePath, extensionsPath, logger, logsPath } = options; const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; const agentFolder = userDataDir; await measureAndLog(promisify(mkdir)(agentFolder), `mkdir(${agentFolder})`, logger); @@ -257,21 +274,23 @@ async function launchServer(options: LaunchOptions) { } async function launchBrowser(options: LaunchOptions, endpoint: string) { - const { logger, workspacePath } = options; + const { logger, workspacePath, tracing, headless } = options; - const browser = await measureAndLog(playwright[options.browser ?? 'chromium'].launch({ headless: options.headless ?? false }), 'playwright#launch', logger); + const browser = await measureAndLog(playwright[options.browser ?? 'chromium'].launch({ headless: headless ?? false }), 'playwright#launch', logger); browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`)); const context = await measureAndLog(browser.newContext(), 'browser.newContext', logger); - try { - await measureAndLog(context.tracing.start({ screenshots: true, snapshots: true, sources: true }), 'context.tracing.start()', logger); - } catch (error) { - logger.log(`Failed to start playwright tracing: ${error}`); // do not fail the build when this fails + if (tracing) { + try { + await measureAndLog(context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + } catch (error) { + logger.log(`Failed to start playwright tracing: ${error}`); // do not fail the build when this fails + } } const page = await measureAndLog(context.newPage(), 'context.newPage()', logger); - await measureAndLog(page.setViewportSize({ width, height }), 'page.setViewportSize', logger); + await measureAndLog(page.setViewportSize({ width: 1200, height: 800 }), 'page.setViewportSize', logger); page.on('pageerror', async (error) => logger.log(`Playwright ERROR: page error: ${error}`)); page.on('crash', () => logger.log('Playwright ERROR: page crash')); @@ -288,8 +307,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) { return { browser, context, page }; } -async function teardown(server: ChildProcess, logger: Logger): Promise { - const serverPid = server.pid; +async function teardown(serverPid: number | undefined, logger: Logger): Promise { if (typeof serverPid !== 'number') { return; } diff --git a/test/automation/src/playwrightElectronDriver.ts b/test/automation/src/playwrightElectronDriver.ts new file mode 100644 index 0000000000000..de6859240e0aa --- /dev/null +++ b/test/automation/src/playwrightElectronDriver.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as playwright from '@playwright/test'; +import { IDriver, IDisposable } from './driver'; +import type { LaunchOptions } from './code'; +import { PlaywrightDriver } from './playwrightBrowserDriver'; +import { IElectronConfiguration, resolveElectronConfiguration } from './electronDriver'; +import { measureAndLog } from './logger'; + +export async function launch(options: LaunchOptions): Promise<{ client: IDisposable; driver: IDriver }> { + + // Resolve electron config and update + const { electronPath, args, env } = await resolveElectronConfiguration(options); + args.push('--enable-smoke-test-driver', 'true'); + + // Launch electron via playwright + const { electron, context, page } = await launchElectron({ electronPath, args, env }, options); + + return { + client: { + dispose: () => { /* there is no client to dispose for electron, teardown is triggered via exitApplication call */ } + }, + driver: new PlaywrightDriver(electron, context, page, undefined /* no server */, options) + }; +} + +async function launchElectron(configuration: IElectronConfiguration, options: LaunchOptions) { + const { logger, tracing } = options; + + const electron = await measureAndLog(playwright._electron.launch({ + executablePath: configuration.electronPath, + args: configuration.args, + env: configuration.env as { [key: string]: string } + }), 'playwright-electron#launch', logger); + + const window = await measureAndLog(electron.firstWindow(), 'playwright-electron#firstWindow', logger); + + const context = window.context(); + + if (tracing) { + try { + await measureAndLog(context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + } catch (error) { + logger.log(`Failed to start playwright tracing: ${error}`); // do not fail the build when this fails + } + } + + window.on('pageerror', async (error) => logger.log(`Playwright ERROR: page error: ${error}`)); + window.on('crash', () => logger.log('Playwright ERROR: page crash')); + window.on('close', () => logger.log('Playwright: page close')); + window.on('response', async (response) => { + if (response.status() >= 400) { + logger.log(`Playwright ERROR: HTTP status ${response.status()} for ${response.url()}`); + } + }); + + return { electron, context, page: window }; +} diff --git a/test/automation/tools/copy-driver-definition.js b/test/automation/tools/copy-driver-definition.js index 5f34e6d7ddc4a..cf757ecbc386b 100644 --- a/test/automation/tools/copy-driver-definition.js +++ b/test/automation/tools/copy-driver-definition.js @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check +'use strict'; + const fs = require('fs'); const path = require('path'); diff --git a/test/automation/tools/copy-package-version.js b/test/automation/tools/copy-package-version.js index f01739a651f1d..b9d1560c4061c 100644 --- a/test/automation/tools/copy-package-version.js +++ b/test/automation/tools/copy-package-version.js @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check +'use strict'; + const fs = require('fs'); const path = require('path'); diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 0510fc4f54d27..18350685d5ea2 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -125,7 +125,7 @@ async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.U const root = path.join(__dirname, '..', '..', '..', '..'); const logsPath = path.join(root, '.build', 'logs', 'integration-tests-browser'); - const serverArgs = ['--driver', 'web', '--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust']; + const serverArgs = ['--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust']; let serverLocation: string; if (process.env.VSCODE_REMOTE_SERVER_PATH) { diff --git a/test/integration/electron/testrunner.js b/test/integration/electron/testrunner.js index 8a64744321b2a..61b8cded132b6 100644 --- a/test/integration/electron/testrunner.js +++ b/test/integration/electron/testrunner.js @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check 'use strict'; const paths = require('path'); @@ -10,7 +11,9 @@ const glob = require('glob'); // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY // Since we are not running in a tty environment, we just implementt he method statically const tty = require('tty'); +// @ts-ignore if (!tty.getWindowSize) { + // @ts-ignore tty.getWindowSize = function () { return [80, 75]; }; } const Mocha = require('mocha'); diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index 6cfa745a11eec..a383d8a5e510f 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -7,7 +7,7 @@ import { join } from 'path'; import { Application, Quality, StatusBarElement, Logger } from '../../../../automation'; import { installAllHandlers } from '../../utils'; -export function setup(isWeb: boolean, logger: Logger) { +export function setup(logger: Logger) { describe('Statusbar', () => { // Shared before/after handling diff --git a/test/smoke/src/areas/workbench/data-loss.test.ts b/test/smoke/src/areas/workbench/data-loss.test.ts index 2749bb696da45..aa6118a16ef4f 100644 --- a/test/smoke/src/areas/workbench/data-loss.test.ts +++ b/test/smoke/src/areas/workbench/data-loss.test.ts @@ -118,7 +118,7 @@ export function setup(ensureStableCode: () => string | undefined, logger: Logger } }); - describe('Data Loss (stable -> insiders)', () => { + describe.skip('Data Loss (stable -> insiders)', () => { //TODO@bpasero enable again once we shipped 1.67.x let insidersApp: Application | undefined = undefined; let stableApp: Application | undefined = undefined; diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 2b8fe48e500a2..39af65bfb0f11 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -13,7 +13,7 @@ import * as rimraf from 'rimraf'; import * as mkdirp from 'mkdirp'; import * as vscodetest from '@vscode/test-electron'; import fetch from 'node-fetch'; -import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog } from '../../automation'; +import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion } from '../../automation'; import { retry, timeout } from './utils'; import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; @@ -28,7 +28,7 @@ import { setup as setupLocalizationTests } from './areas/workbench/localization. import { setup as setupLaunchTests } from './areas/workbench/launch.test'; import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; -const repoPath = path.join(__dirname, '..', '..', '..'); +const rootPath = path.join(__dirname, '..', '..', '..'); const [, , ...args] = process.argv; const opts = minimist(args, { @@ -44,7 +44,9 @@ const opts = minimist(args, { 'verbose', 'remote', 'web', - 'headless' + 'headless', + 'legacy', + 'tracing' ], default: { verbose: false @@ -54,12 +56,29 @@ const opts = minimist(args, { remote?: boolean; headless?: boolean; web?: boolean; + legacy?: boolean; + tracing?: boolean; build?: string; 'stable-build'?: string; browser?: string; electronArgs?: string; }; +const logsPath = (() => { + const logsParentPath = path.join(rootPath, '.build', 'logs'); + + let logsName: string; + if (opts.web) { + logsName = 'smoke-tests-browser'; + } else if (opts.remote) { + logsName = opts.legacy ? 'smoke-tests-remote-legacy' : 'smoke-tests-remote'; + } else { + logsName = opts.legacy ? 'smoke-tests-electron-legacy' : 'smoke-tests-electron'; + } + + return path.join(logsParentPath, logsName); +})(); + const logger = createLogger(); function createLogger(): Logger { @@ -70,10 +89,12 @@ function createLogger(): Logger { loggers.push(new ConsoleLogger()); } + // Prepare logs path + fs.rmSync(logsPath, { recursive: true, force: true, maxRetries: 3 }); + mkdirp.sync(logsPath); + // Always log to log file - const logPath = path.join(repoPath, '.build', 'logs', opts.web ? 'smoke-tests-browser' : opts.remote ? 'smoke-tests-remote' : 'smoke-tests'); - mkdirp.sync(logPath); - loggers.push(new FileLogger(path.join(logPath, 'smoke-test-runner.log'))); + loggers.push(new FileLogger(path.join(logsPath, 'smoke-test-runner.log'))); return new MultiLogger(loggers); } @@ -122,49 +143,6 @@ function parseVersion(version: string): { major: number; minor: number; patch: n // #### Electron Smoke Tests #### // if (!opts.web) { - - function getDevElectronPath(): string { - const buildPath = path.join(repoPath, '.build'); - const product = require(path.join(repoPath, 'product.json')); - - switch (process.platform) { - case 'darwin': - return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron'); - case 'linux': - return path.join(buildPath, 'electron', `${product.applicationName}`); - case 'win32': - return path.join(buildPath, 'electron', `${product.nameShort}.exe`); - default: - throw new Error('Unsupported platform.'); - } - } - - function getBuildElectronPath(root: string): string { - switch (process.platform) { - case 'darwin': - return path.join(root, 'Contents', 'MacOS', 'Electron'); - case 'linux': { - const product = require(path.join(root, 'resources', 'app', 'product.json')); - return path.join(root, product.applicationName); - } - case 'win32': { - const product = require(path.join(root, 'resources', 'app', 'product.json')); - return path.join(root, `${product.nameShort}.exe`); - } - default: - throw new Error('Unsupported platform.'); - } - } - - function getBuildVersion(root: string): string { - switch (process.platform) { - case 'darwin': - return require(path.join(root, 'Contents', 'Resources', 'app', 'package.json')).version; - default: - return require(path.join(root, 'resources', 'app', 'package.json')).version; - } - } - let testCodePath = opts.build; let electronPath: string; @@ -174,7 +152,7 @@ if (!opts.web) { } else { testCodePath = getDevElectronPath(); electronPath = testCodePath; - process.env.VSCODE_REPOSITORY = repoPath; + process.env.VSCODE_REPOSITORY = rootPath; process.env.VSCODE_DEV = '1'; process.env.VSCODE_CLI = '1'; } @@ -213,7 +191,7 @@ else { } if (!testCodeServerPath) { - process.env.VSCODE_REPOSITORY = repoPath; + process.env.VSCODE_REPOSITORY = rootPath; process.env.VSCODE_DEV = '1'; process.env.VSCODE_CLI = '1'; @@ -355,9 +333,12 @@ before(async function () { extensionsPath, waitTime: parseInt(opts['wait-time'] || '0') || 20, logger, + logsPath, verbose: opts.verbose, remote: opts.remote, web: opts.web, + legacy: opts.legacy, + tracing: opts.tracing, headless: opts.headless, browser: opts.browser, extraArgs: (opts.electronArgs || '').split(' ').map(a => a.trim()).filter(a => !!a) @@ -390,14 +371,14 @@ after(async function () { } }); -describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { +describe(`VSCode Smoke Tests (${opts.web ? 'Web' : opts.legacy ? 'Electron (legacy)' : 'Electron'})`, () => { if (!opts.web) { setupDataLossTests(() => opts['stable-build'] /* Do not change, deferred for a reason! */, logger); } - if (!opts.web) { setupPreferencesTests(logger); } + setupPreferencesTests(logger); setupSearchTests(logger); setupNotebookTests(logger); setupLanguagesTests(logger); - if (opts.web) { setupTerminalTests(logger); } // TODO@daniel TODO@meggan: Enable terminal tests for non-web when the desktop driver is moved to playwright - setupStatusbarTests(!!opts.web, logger); + setupTerminalTests(logger); + setupStatusbarTests(logger); if (quality !== Quality.Dev) { setupExtensionTests(logger); } if (!opts.web) { setupMultirootTests(logger); } if (!opts.web && !opts.remote && quality !== Quality.Dev) { setupLocalizationTests(logger); } diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 276bcfe37b6c1..7b2894db1e1fb 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -3,17 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const path = require('path'); +//@ts-check +'use strict'; + +const { join } = require('path'); const Mocha = require('mocha'); const minimist = require('minimist'); const [, , ...args] = process.argv; const opts = minimist(args, { - boolean: 'web', + boolean: ['web', 'legacy'], string: ['f', 'g'] }); -const suite = opts['web'] ? 'Browser Smoke Tests' : 'Desktop Smoke Tests'; +const suite = opts['web'] ? 'Browser Smoke Tests' : opts['legacy'] ? 'Desktop Smoke Tests (Legacy)' : 'Desktop Smoke Tests'; const options = { color: true, @@ -28,7 +31,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } @@ -39,9 +42,8 @@ mocha.run(failures => { // Indicate location of log files for further diagnosis if (failures) { - const repoPath = path.join(__dirname, '..', '..', '..'); - const logPath = path.join(repoPath, '.build', 'logs', opts.web ? 'smoke-tests-browser' : opts.remote ? 'smoke-tests-remote' : 'smoke-tests'); - const logFile = path.join(logPath, 'smoke-test-runner.log'); + const rootPath = join(__dirname, '..', '..', '..'); + const logPath = join(rootPath, '.build', 'logs'); if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { console.log(` @@ -62,7 +64,7 @@ mocha.run(failures => { # '${logPath}'. # # Logs of the smoke test runner are stored into -# '${logFile}'. +# 'smoke-test-runner.log' in respective folder. # ############################################# `); diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 6e8a61872a6a5..f8f871d6d3b5c 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; const path = require('path'); const glob = require('glob'); diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 8ef90e8dc4ee1..f423c6d64fdd0 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; process.env.MOCHA_COLORS = '1'; // Force colors (note that this must come before any mocha imports)