Path: blob/main/test/automation/src/playwrightDriver.ts
3520 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as playwright from '@playwright/test';6import type { Protocol } from 'playwright-core/types/protocol';7import { dirname, join } from 'path';8import { promises } from 'fs';9import { IWindowDriver } from './driver';10import { PageFunction } from 'playwright-core/types/structs';11import { measureAndLog } from './logger';12import { LaunchOptions } from './code';13import { teardown } from './processes';14import { ChildProcess } from 'child_process';1516export class PlaywrightDriver {1718private static traceCounter = 1;19private static screenShotCounter = 1;2021private static readonly vscodeToPlaywrightKey: { [key: string]: string } = {22cmd: 'Meta',23ctrl: 'Control',24shift: 'Shift',25enter: 'Enter',26escape: 'Escape',27right: 'ArrowRight',28up: 'ArrowUp',29down: 'ArrowDown',30left: 'ArrowLeft',31home: 'Home',32esc: 'Escape'33};3435constructor(36private readonly application: playwright.Browser | playwright.ElectronApplication,37private readonly context: playwright.BrowserContext,38private readonly page: playwright.Page,39private readonly serverProcess: ChildProcess | undefined,40private readonly whenLoaded: Promise<unknown>,41private readonly options: LaunchOptions42) {43}4445get browserContext(): playwright.BrowserContext {46return this.context;47}4849get currentPage(): playwright.Page {50return this.page;51}5253async startTracing(name: string): Promise<void> {54if (!this.options.tracing) {55return; // tracing disabled56}5758try {59await measureAndLog(() => this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.options.logger);60} catch (error) {61// Ignore62}63}6465async stopTracing(name: string, persist: boolean): Promise<void> {66if (!this.options.tracing) {67return; // tracing disabled68}6970try {71let persistPath: string | undefined = undefined;72if (persist) {73persistPath = join(this.options.logsPath, `playwright-trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`);74}7576await measureAndLog(() => this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.options.logger);7778// To ensure we have a screenshot at the end where79// it failed, also trigger one explicitly. Tracing80// does not guarantee to give us a screenshot unless81// some driver action ran before.82if (persist) {83await this.takeScreenshot(name);84}85} catch (error) {86// Ignore87}88}8990async didFinishLoad(): Promise<void> {91await this.whenLoaded;92}9394private _cdpSession: playwright.CDPSession | undefined;9596async startCDP() {97if (this._cdpSession) {98return;99}100101this._cdpSession = await this.page.context().newCDPSession(this.page);102}103104async collectGarbage() {105if (!this._cdpSession) {106throw new Error('CDP not started');107}108109await this._cdpSession.send('HeapProfiler.collectGarbage');110}111112async evaluate(options: Protocol.Runtime.evaluateParameters): Promise<Protocol.Runtime.evaluateReturnValue> {113if (!this._cdpSession) {114throw new Error('CDP not started');115}116117return await this._cdpSession.send('Runtime.evaluate', options);118}119120async releaseObjectGroup(parameters: Protocol.Runtime.releaseObjectGroupParameters): Promise<void> {121if (!this._cdpSession) {122throw new Error('CDP not started');123}124125await this._cdpSession.send('Runtime.releaseObjectGroup', parameters);126}127128async queryObjects(parameters: Protocol.Runtime.queryObjectsParameters): Promise<Protocol.Runtime.queryObjectsReturnValue> {129if (!this._cdpSession) {130throw new Error('CDP not started');131}132133return await this._cdpSession.send('Runtime.queryObjects', parameters);134}135136async callFunctionOn(parameters: Protocol.Runtime.callFunctionOnParameters): Promise<Protocol.Runtime.callFunctionOnReturnValue> {137if (!this._cdpSession) {138throw new Error('CDP not started');139}140141return await this._cdpSession.send('Runtime.callFunctionOn', parameters);142}143144async takeHeapSnapshot(): Promise<string> {145if (!this._cdpSession) {146throw new Error('CDP not started');147}148149let snapshot = '';150const listener = (c: { chunk: string }) => {151snapshot += c.chunk;152};153154this._cdpSession.addListener('HeapProfiler.addHeapSnapshotChunk', listener);155156await this._cdpSession.send('HeapProfiler.takeHeapSnapshot');157158this._cdpSession.removeListener('HeapProfiler.addHeapSnapshotChunk', listener);159return snapshot;160}161162async getProperties(parameters: Protocol.Runtime.getPropertiesParameters): Promise<Protocol.Runtime.getPropertiesReturnValue> {163if (!this._cdpSession) {164throw new Error('CDP not started');165}166167return await this._cdpSession.send('Runtime.getProperties', parameters);168}169170private async takeScreenshot(name: string): Promise<void> {171try {172const persistPath = join(this.options.logsPath, `playwright-screenshot-${PlaywrightDriver.screenShotCounter++}-${name.replace(/\s+/g, '-')}.png`);173174await measureAndLog(() => this.page.screenshot({ path: persistPath, type: 'png' }), 'takeScreenshot', this.options.logger);175} catch (error) {176// Ignore177}178}179180async reload() {181await this.page.reload();182}183184async close() {185186// Stop tracing187try {188if (this.options.tracing) {189await measureAndLog(() => this.context.tracing.stop(), 'stop tracing', this.options.logger);190}191} catch (error) {192// Ignore193}194195// Web: Extract client logs196if (this.options.web) {197try {198await measureAndLog(() => this.saveWebClientLogs(), 'saveWebClientLogs()', this.options.logger);199} catch (error) {200this.options.logger.log(`Error saving web client logs (${error})`);201}202}203204// exit via `close` method205try {206await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger);207} catch (error) {208this.options.logger.log(`Error closing application (${error})`);209}210211// Server: via `teardown`212if (this.serverProcess) {213await measureAndLog(() => teardown(this.serverProcess!, this.options.logger), 'teardown server process', this.options.logger);214}215}216217private async saveWebClientLogs(): Promise<void> {218const logs = await this.getLogs();219220for (const log of logs) {221const absoluteLogsPath = join(this.options.logsPath, log.relativePath);222223await promises.mkdir(dirname(absoluteLogsPath), { recursive: true });224await promises.writeFile(absoluteLogsPath, log.contents);225}226}227228async sendKeybinding(keybinding: string, accept?: () => Promise<void> | void) {229const chords = keybinding.split(' ');230for (let i = 0; i < chords.length; i++) {231const chord = chords[i];232if (i > 0) {233await this.wait(100);234}235236if (keybinding.startsWith('Alt') || keybinding.startsWith('Control') || keybinding.startsWith('Backspace')) {237await this.page.keyboard.press(keybinding);238return;239}240241const keys = chord.split('+');242const keysDown: string[] = [];243for (let i = 0; i < keys.length; i++) {244if (keys[i] in PlaywrightDriver.vscodeToPlaywrightKey) {245keys[i] = PlaywrightDriver.vscodeToPlaywrightKey[keys[i]];246}247await this.page.keyboard.down(keys[i]);248keysDown.push(keys[i]);249}250while (keysDown.length > 0) {251await this.page.keyboard.up(keysDown.pop()!);252}253}254255await accept?.();256}257258async click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined) {259const { x, y } = await this.getElementXY(selector, xoffset, yoffset);260await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));261}262263async setValue(selector: string, text: string) {264return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this.getDriverHandle(), selector, text] as const);265}266267async getTitle() {268return this.page.title();269}270271async isActiveElement(selector: string) {272return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this.getDriverHandle(), selector] as const);273}274275async getElements(selector: string, recursive: boolean = false) {276return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this.getDriverHandle(), selector, recursive] as const);277}278279async getElementXY(selector: string, xoffset?: number, yoffset?: number) {280return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this.getDriverHandle(), selector, xoffset, yoffset] as const);281}282283async typeInEditor(selector: string, text: string) {284return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const);285}286287async getEditorSelection(selector: string) {288return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const);289}290291async getTerminalBuffer(selector: string) {292return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const);293}294295async writeInTerminal(selector: string, text: string) {296return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this.getDriverHandle(), selector, text] as const);297}298299async getLocaleInfo() {300return this.evaluateWithDriver(([driver]) => driver.getLocaleInfo());301}302303async getLocalizedStrings() {304return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings());305}306307async getLogs() {308return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const);309}310311private async evaluateWithDriver<T>(pageFunction: PageFunction<IWindowDriver[], T>) {312return this.page.evaluate(pageFunction, [await this.getDriverHandle()]);313}314315wait(ms: number): Promise<void> {316return wait(ms);317}318319whenWorkbenchRestored(): Promise<void> {320return this.evaluateWithDriver(([driver]) => driver.whenWorkbenchRestored());321}322323private async getDriverHandle(): Promise<playwright.JSHandle<IWindowDriver>> {324return this.page.evaluateHandle('window.driver');325}326327async isAlive(): Promise<boolean> {328try {329await this.getDriverHandle();330return true;331} catch (error) {332return false;333}334}335}336337export function wait(ms: number): Promise<void> {338return new Promise<void>(resolve => setTimeout(resolve, ms));339}340341342