mirror of
https://github.com/microsoft/vscode.git
synced 2026-03-03 15:29:23 +00:00
Use playwright for desktop smoke tests (#146692)
* Use `playwright` for desktop smoke tests * fix distro issues * tests - enable prefs tests for web
This commit is contained in:
@@ -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'))
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>;
|
||||
abstract doubleClick(selector: string): Promise<void>;
|
||||
|
||||
async setValue(selector: string, text: string): Promise<void> {
|
||||
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<string> {
|
||||
return document.title;
|
||||
}
|
||||
|
||||
async isActiveElement(selector: string): Promise<boolean> {
|
||||
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<IElement[]> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<ILocaleInfo> {
|
||||
return Promise.resolve({
|
||||
language: language,
|
||||
locale: locale
|
||||
});
|
||||
}
|
||||
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings> {
|
||||
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<void>;
|
||||
}
|
||||
@@ -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<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
export class BrowserWindowDriver implements IWindowDriver {
|
||||
|
||||
async setValue(selector: string, text: string): Promise<void> {
|
||||
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<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
async getTitle(): Promise<string> {
|
||||
return document.title;
|
||||
}
|
||||
openDevTools(): Promise<void> {
|
||||
|
||||
async isActiveElement(selector: string): Promise<boolean> {
|
||||
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<IElement[]> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<ILocaleInfo> {
|
||||
return Promise.resolve({
|
||||
language: language,
|
||||
locale: locale
|
||||
});
|
||||
}
|
||||
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings> {
|
||||
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<void> {
|
||||
|
||||
// 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<IDisposable> {
|
||||
(<any>window).driver = new BrowserWindowDriver();
|
||||
|
||||
return Disposable.None;
|
||||
export function registerWindowDriver(): void {
|
||||
Object.assign(window, { driver: new BrowserWindowDriver() });
|
||||
}
|
||||
|
||||
@@ -44,10 +44,9 @@ export interface IDriver {
|
||||
startTracing(windowId: number, name: string): Promise<void>;
|
||||
stopTracing(windowId: number, name: string, persist: boolean): Promise<void>;
|
||||
reloadWindow(windowId: number): Promise<void>;
|
||||
exitApplication(): Promise<boolean>;
|
||||
exitApplication(): Promise<number /* main PID */>;
|
||||
dispatchKeybinding(windowId: number, keybinding: string): Promise<void>;
|
||||
click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
|
||||
doubleClick(windowId: number, selector: string): Promise<void>;
|
||||
setValue(windowId: number, selector: string, text: string): Promise<void>;
|
||||
getTitle(windowId: number): Promise<string>;
|
||||
isActiveElement(windowId: number, selector: string): Promise<boolean>;
|
||||
@@ -62,7 +61,6 @@ export interface IDriver {
|
||||
|
||||
export interface IWindowDriver {
|
||||
click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
|
||||
doubleClick(selector: string): Promise<void>;
|
||||
setValue(selector: string, text: string): Promise<void>;
|
||||
getTitle(): Promise<string>;
|
||||
isActiveElement(selector: string): Promise<boolean>;
|
||||
@@ -79,11 +77,7 @@ export interface IWindowDriver {
|
||||
export const ID = 'driverService';
|
||||
export const IDriver = createDecorator<IDriver>(ID);
|
||||
|
||||
export interface IDriverOptions {
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export interface IWindowDriverRegistry {
|
||||
registerWindowDriver(windowId: number): Promise<IDriverOptions>;
|
||||
registerWindowDriver(windowId: number): Promise<void>;
|
||||
reloadWindowDriver(windowId: number): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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<any> {
|
||||
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<void> {
|
||||
return this.channel.call('doubleClick', selector);
|
||||
}
|
||||
|
||||
setValue(selector: string, text: string): Promise<void> {
|
||||
return this.channel.call('setValue', [selector, text]);
|
||||
}
|
||||
@@ -96,7 +91,7 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
registerWindowDriver(windowId: number): Promise<IDriverOptions> {
|
||||
registerWindowDriver(windowId: number): Promise<void> {
|
||||
return this.channel.call('registerWindowDriver', windowId);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IDriverOptions> {
|
||||
async registerWindowDriver(windowId: number): Promise<void> {
|
||||
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<void> {
|
||||
@@ -111,10 +109,12 @@ export class Driver implements IDriver, IWindowDriverRegistry {
|
||||
this.lifecycleMainService.reload(window);
|
||||
}
|
||||
|
||||
exitApplication(): Promise<boolean> {
|
||||
async exitApplication(): Promise<number> {
|
||||
this.logService.info(`[driver] exitApplication()`);
|
||||
|
||||
return this.lifecycleMainService.quit();
|
||||
this.lifecycleMainService.quit();
|
||||
|
||||
return process.pid;
|
||||
}
|
||||
|
||||
async dispatchKeybinding(windowId: number, keybinding: string): Promise<void> {
|
||||
@@ -175,11 +175,6 @@ export class Driver implements IDriver, IWindowDriverRegistry {
|
||||
await windowDriver.click(selector, xoffset, yoffset);
|
||||
}
|
||||
|
||||
async doubleClick(windowId: number, selector: string): Promise<void> {
|
||||
const windowDriver = await this.getWindowDriver(windowId);
|
||||
await windowDriver.doubleClick(selector);
|
||||
}
|
||||
|
||||
async setValue(windowId: number, selector: string, text: string): Promise<void> {
|
||||
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<IDisposable> {
|
||||
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);
|
||||
|
||||
@@ -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<number /* main process PID */>;
|
||||
}
|
||||
|
||||
class NativeWindowDriver extends BrowserWindowDriver {
|
||||
|
||||
constructor(private readonly helper: INativeWindowDriverHelper) {
|
||||
super();
|
||||
}
|
||||
|
||||
exitApplication(): Promise<number> {
|
||||
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<void> {
|
||||
override click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
|
||||
const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;
|
||||
return this._click(selector, 1, offset);
|
||||
|
||||
return this.doClick(selector, 1, offset);
|
||||
}
|
||||
|
||||
doubleClick(selector: string): Promise<void> {
|
||||
return this._click(selector, 2);
|
||||
}
|
||||
|
||||
private async _click(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise<void> {
|
||||
private async doClick(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise<void> {
|
||||
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<void> {
|
||||
await this.nativeHostService.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> {
|
||||
/**
|
||||
* Old school window driver that is implemented by us
|
||||
* from the main process.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export async function registerLegacyWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<boolean> {
|
||||
exitApplication(): Promise<number> {
|
||||
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<void> {
|
||||
return this.channel.call('doubleClick', [windowId, selector]);
|
||||
}
|
||||
|
||||
setValue(windowId: number, selector: string, text: string): Promise<void> {
|
||||
return this.channel.call('setValue', [windowId, selector, text]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']; }
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'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<Required<NativeParsedArgs>> = {
|
||||
'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' },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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<number> {
|
||||
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 {
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Code> {
|
||||
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<void> {
|
||||
|
||||
// Start the exit flow via driver
|
||||
const pid = await measureAndLog(this.driver.exitApplication(), 'driver.exitApplication()', this.logger);
|
||||
|
||||
return measureAndLog(new Promise<void>((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;
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(resolve, reject), reject));
|
||||
};
|
||||
|
||||
@@ -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<void> }> {
|
||||
const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, verbose, extraArgs } = options;
|
||||
export interface IElectronConfiguration {
|
||||
readonly electronPath: string;
|
||||
readonly args: string[];
|
||||
readonly env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
export async function resolveElectronConfiguration(options: LaunchOptions): Promise<IElectronConfiguration> {
|
||||
const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, logsPath, 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();
|
||||
|
||||
const driverIPCHandle = await measureAndLog(createDriverHandle(), 'createDriverHandle', logger);
|
||||
|
||||
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<void> }> {
|
||||
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<string> {
|
||||
|
||||
// Windows
|
||||
if ('win32' === os.platform()) {
|
||||
if ('win32' === platform()) {
|
||||
const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join('');
|
||||
return `\\\\.\\pipe\\${name}`;
|
||||
}
|
||||
|
||||
@@ -26,3 +26,4 @@ export * from './viewlet';
|
||||
export * from './localization';
|
||||
export * from './workbench';
|
||||
export * from './driver';
|
||||
export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electronDriver';
|
||||
|
||||
@@ -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;
|
||||
export class PlaywrightDriver implements IDriver {
|
||||
|
||||
const root = join(__dirname, '..', '..', '..');
|
||||
const logsPath = join(root, '.build', 'logs', 'smoke-tests-browser');
|
||||
private static traceCounter = 1;
|
||||
|
||||
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 {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
await measureAndLog(this.browser.close(), 'Browser.close()', this.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})`);
|
||||
}
|
||||
}
|
||||
|
||||
await measureAndLog(teardown(this.server, this.logger), 'teardown server', this.logger);
|
||||
// Playwright shutdown
|
||||
try {
|
||||
await measureAndLog(this.application.close(), 'playwright.close()', this.options.logger);
|
||||
} catch (error) {
|
||||
this.options.logger.log(`Error closing appliction (${error})`);
|
||||
}
|
||||
|
||||
return false;
|
||||
// Server shutdown
|
||||
if (typeof this.serverPid === 'number') {
|
||||
await measureAndLog(teardown(this.serverPid, this.options.logger), 'teardown server', this.options.logger);
|
||||
}
|
||||
|
||||
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<void>(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// TODO: Cache
|
||||
private async _getDriverHandle(): Promise<playwright.JSHandle<IWindowDriver>> {
|
||||
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<void> }> {
|
||||
@@ -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<void> {
|
||||
const serverPid = server.pid;
|
||||
async function teardown(serverPid: number | undefined, logger: Logger): Promise<void> {
|
||||
if (typeof serverPid !== 'number') {
|
||||
return;
|
||||
}
|
||||
61
test/automation/src/playwrightElectronDriver.ts
Normal file
61
test/automation/src/playwrightElectronDriver.ts
Normal file
@@ -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 };
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
#############################################
|
||||
`);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
process.env.MOCHA_COLORS = '1'; // Force colors (note that this must come before any mocha imports)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user