mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 20:13:32 +01:00
* adding code for smoke tests * adding awaits * changing wait code * renaming dispatch to send * moving comment * update to send
337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 type { Protocol } from 'playwright-core/types/protocol';
|
|
import { dirname, join } from 'path';
|
|
import { promises } from 'fs';
|
|
import { IWindowDriver } from './driver';
|
|
import { PageFunction } from 'playwright-core/types/structs';
|
|
import { measureAndLog } from './logger';
|
|
import { LaunchOptions } from './code';
|
|
import { teardown } from './processes';
|
|
import { ChildProcess } from 'child_process';
|
|
|
|
export class PlaywrightDriver {
|
|
|
|
private static traceCounter = 1;
|
|
private static screenShotCounter = 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'
|
|
};
|
|
|
|
constructor(
|
|
private readonly application: playwright.Browser | playwright.ElectronApplication,
|
|
private readonly context: playwright.BrowserContext,
|
|
private readonly page: playwright.Page,
|
|
private readonly serverProcess: ChildProcess | undefined,
|
|
private readonly whenLoaded: Promise<unknown>,
|
|
private readonly options: LaunchOptions
|
|
) {
|
|
}
|
|
|
|
async startTracing(name: string): Promise<void> {
|
|
if (!this.options.tracing) {
|
|
return; // tracing disabled
|
|
}
|
|
|
|
try {
|
|
await measureAndLog(() => this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.options.logger);
|
|
} catch (error) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
async stopTracing(name: string, persist: boolean): Promise<void> {
|
|
if (!this.options.tracing) {
|
|
return; // tracing disabled
|
|
}
|
|
|
|
try {
|
|
let persistPath: string | undefined = undefined;
|
|
if (persist) {
|
|
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.options.logger);
|
|
|
|
// To ensure we have a screenshot at the end where
|
|
// it failed, also trigger one explicitly. Tracing
|
|
// does not guarantee to give us a screenshot unless
|
|
// some driver action ran before.
|
|
if (persist) {
|
|
await this.takeScreenshot(name);
|
|
}
|
|
} catch (error) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
async didFinishLoad(): Promise<void> {
|
|
await this.whenLoaded;
|
|
}
|
|
|
|
private _cdpSession: playwright.CDPSession | undefined;
|
|
|
|
async startCDP() {
|
|
if (this._cdpSession) {
|
|
return;
|
|
}
|
|
|
|
this._cdpSession = await this.page.context().newCDPSession(this.page);
|
|
}
|
|
|
|
async collectGarbage() {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
await this._cdpSession.send('HeapProfiler.collectGarbage');
|
|
}
|
|
|
|
async evaluate(options: Protocol.Runtime.evaluateParameters): Promise<Protocol.Runtime.evaluateReturnValue> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
return await this._cdpSession.send('Runtime.evaluate', options);
|
|
}
|
|
|
|
async releaseObjectGroup(parameters: Protocol.Runtime.releaseObjectGroupParameters): Promise<void> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
await this._cdpSession.send('Runtime.releaseObjectGroup', parameters);
|
|
}
|
|
|
|
async queryObjects(parameters: Protocol.Runtime.queryObjectsParameters): Promise<Protocol.Runtime.queryObjectsReturnValue> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
return await this._cdpSession.send('Runtime.queryObjects', parameters);
|
|
}
|
|
|
|
async callFunctionOn(parameters: Protocol.Runtime.callFunctionOnParameters): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
return await this._cdpSession.send('Runtime.callFunctionOn', parameters);
|
|
}
|
|
|
|
async takeHeapSnapshot(): Promise<string> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
let snapshot = '';
|
|
const listener = (c: { chunk: string }) => {
|
|
snapshot += c.chunk;
|
|
};
|
|
|
|
this._cdpSession.addListener('HeapProfiler.addHeapSnapshotChunk', listener);
|
|
|
|
await this._cdpSession.send('HeapProfiler.takeHeapSnapshot');
|
|
|
|
this._cdpSession.removeListener('HeapProfiler.addHeapSnapshotChunk', listener);
|
|
return snapshot;
|
|
}
|
|
|
|
async getProperties(parameters: Protocol.Runtime.getPropertiesParameters): Promise<Protocol.Runtime.getPropertiesReturnValue> {
|
|
if (!this._cdpSession) {
|
|
throw new Error('CDP not started');
|
|
}
|
|
|
|
return await this._cdpSession.send('Runtime.getProperties', parameters);
|
|
}
|
|
|
|
private async takeScreenshot(name: string): Promise<void> {
|
|
try {
|
|
const persistPath = join(this.options.logsPath, `playwright-screenshot-${PlaywrightDriver.screenShotCounter++}-${name.replace(/\s+/g, '-')}.png`);
|
|
|
|
await measureAndLog(() => this.page.screenshot({ path: persistPath, type: 'png' }), 'takeScreenshot', this.options.logger);
|
|
} catch (error) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
async reload() {
|
|
await this.page.reload();
|
|
}
|
|
|
|
async exitApplication() {
|
|
|
|
// Stop tracing
|
|
try {
|
|
if (this.options.tracing) {
|
|
await measureAndLog(() => this.context.tracing.stop(), 'stop tracing', this.options.logger);
|
|
}
|
|
} catch (error) {
|
|
// Ignore
|
|
}
|
|
|
|
// Web: Extract client logs
|
|
if (this.options.web) {
|
|
try {
|
|
await measureAndLog(() => this.saveWebClientLogs(), 'saveWebClientLogs()', this.options.logger);
|
|
} catch (error) {
|
|
this.options.logger.log(`Error saving web client logs (${error})`);
|
|
}
|
|
}
|
|
|
|
// Web: exit via `close` method
|
|
if (this.options.web) {
|
|
try {
|
|
await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger);
|
|
} catch (error) {
|
|
this.options.logger.log(`Error closing appliction (${error})`);
|
|
}
|
|
}
|
|
|
|
// Desktop: exit via `driver.exitApplication`
|
|
else {
|
|
try {
|
|
await measureAndLog(() => this.evaluateWithDriver(([driver]) => driver.exitApplication()), 'driver.exitApplication()', this.options.logger);
|
|
} catch (error) {
|
|
this.options.logger.log(`Error exiting appliction (${error})`);
|
|
}
|
|
}
|
|
|
|
// Server: via `teardown`
|
|
if (this.serverProcess) {
|
|
await measureAndLog(() => teardown(this.serverProcess!, this.options.logger), 'teardown server process', this.options.logger);
|
|
}
|
|
}
|
|
|
|
private async saveWebClientLogs(): Promise<void> {
|
|
const logs = await this.getLogs();
|
|
|
|
for (const log of logs) {
|
|
const absoluteLogsPath = join(this.options.logsPath, log.relativePath);
|
|
|
|
await promises.mkdir(dirname(absoluteLogsPath), { recursive: true });
|
|
await promises.writeFile(absoluteLogsPath, log.contents);
|
|
}
|
|
}
|
|
|
|
async sendKeybinding(keybinding: string, accept?: () => Promise<void> | void) {
|
|
const chords = keybinding.split(' ');
|
|
for (let i = 0; i < chords.length; i++) {
|
|
const chord = chords[i];
|
|
if (i > 0) {
|
|
await this.wait(100);
|
|
}
|
|
|
|
if (keybinding.startsWith('Alt') || keybinding.startsWith('Control') || keybinding.startsWith('Backspace')) {
|
|
await this.page.keyboard.press(keybinding);
|
|
return;
|
|
}
|
|
|
|
const keys = chord.split('+');
|
|
const keysDown: string[] = [];
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (keys[i] in PlaywrightDriver.vscodeToPlaywrightKey) {
|
|
keys[i] = PlaywrightDriver.vscodeToPlaywrightKey[keys[i]];
|
|
}
|
|
await this.page.keyboard.down(keys[i]);
|
|
keysDown.push(keys[i]);
|
|
}
|
|
while (keysDown.length > 0) {
|
|
await this.page.keyboard.up(keysDown.pop()!);
|
|
}
|
|
}
|
|
|
|
if (accept) {
|
|
await accept();
|
|
}
|
|
}
|
|
|
|
async click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined) {
|
|
const { x, y } = await this.getElementXY(selector, xoffset, yoffset);
|
|
await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));
|
|
}
|
|
|
|
async setValue(selector: string, text: string) {
|
|
return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this.getDriverHandle(), selector, text] as const);
|
|
}
|
|
|
|
async getTitle() {
|
|
return this.page.title();
|
|
}
|
|
|
|
async isActiveElement(selector: string) {
|
|
return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this.getDriverHandle(), selector] as const);
|
|
}
|
|
|
|
async getElements(selector: string, recursive: boolean = false) {
|
|
return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this.getDriverHandle(), selector, recursive] as const);
|
|
}
|
|
|
|
async getElementXY(selector: string, xoffset?: number, yoffset?: number) {
|
|
return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this.getDriverHandle(), selector, xoffset, yoffset] as const);
|
|
}
|
|
|
|
async typeInEditor(selector: string, text: string) {
|
|
return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const);
|
|
}
|
|
|
|
async getEditorSelection(selector: string) {
|
|
return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const);
|
|
}
|
|
|
|
async getTerminalBuffer(selector: string) {
|
|
return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const);
|
|
}
|
|
|
|
async writeInTerminal(selector: string, text: string) {
|
|
return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this.getDriverHandle(), selector, text] as const);
|
|
}
|
|
|
|
async getLocaleInfo() {
|
|
return this.evaluateWithDriver(([driver]) => driver.getLocaleInfo());
|
|
}
|
|
|
|
async getLocalizedStrings() {
|
|
return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings());
|
|
}
|
|
|
|
async getLogs() {
|
|
return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const);
|
|
}
|
|
|
|
private async evaluateWithDriver<T>(pageFunction: PageFunction<IWindowDriver[], T>) {
|
|
return this.page.evaluate(pageFunction, [await this.getDriverHandle()]);
|
|
}
|
|
|
|
wait(ms: number): Promise<void> {
|
|
return wait(ms);
|
|
}
|
|
|
|
whenWorkbenchRestored(): Promise<void> {
|
|
return this.evaluateWithDriver(([driver]) => driver.whenWorkbenchRestored());
|
|
}
|
|
|
|
private async getDriverHandle(): Promise<playwright.JSHandle<IWindowDriver>> {
|
|
return this.page.evaluateHandle('window.driver');
|
|
}
|
|
}
|
|
|
|
export function wait(ms: number): Promise<void> {
|
|
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
}
|