Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/automation/src/playwrightDriver.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as playwright from '@playwright/test';
7
import type { Protocol } from 'playwright-core/types/protocol';
8
import { dirname, join } from 'path';
9
import { promises } from 'fs';
10
import { IWindowDriver } from './driver';
11
import { PageFunction } from 'playwright-core/types/structs';
12
import { measureAndLog } from './logger';
13
import { LaunchOptions } from './code';
14
import { teardown } from './processes';
15
import { ChildProcess } from 'child_process';
16
17
export class PlaywrightDriver {
18
19
private static traceCounter = 1;
20
private static screenShotCounter = 1;
21
22
private static readonly vscodeToPlaywrightKey: { [key: string]: string } = {
23
cmd: 'Meta',
24
ctrl: 'Control',
25
shift: 'Shift',
26
enter: 'Enter',
27
escape: 'Escape',
28
right: 'ArrowRight',
29
up: 'ArrowUp',
30
down: 'ArrowDown',
31
left: 'ArrowLeft',
32
home: 'Home',
33
esc: 'Escape'
34
};
35
36
constructor(
37
private readonly application: playwright.Browser | playwright.ElectronApplication,
38
private readonly context: playwright.BrowserContext,
39
private readonly page: playwright.Page,
40
private readonly serverProcess: ChildProcess | undefined,
41
private readonly whenLoaded: Promise<unknown>,
42
private readonly options: LaunchOptions
43
) {
44
}
45
46
get browserContext(): playwright.BrowserContext {
47
return this.context;
48
}
49
50
get currentPage(): playwright.Page {
51
return this.page;
52
}
53
54
async startTracing(name: string): Promise<void> {
55
if (!this.options.tracing) {
56
return; // tracing disabled
57
}
58
59
try {
60
await measureAndLog(() => this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.options.logger);
61
} catch (error) {
62
// Ignore
63
}
64
}
65
66
async stopTracing(name: string, persist: boolean): Promise<void> {
67
if (!this.options.tracing) {
68
return; // tracing disabled
69
}
70
71
try {
72
let persistPath: string | undefined = undefined;
73
if (persist) {
74
persistPath = join(this.options.logsPath, `playwright-trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`);
75
}
76
77
await measureAndLog(() => this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.options.logger);
78
79
// To ensure we have a screenshot at the end where
80
// it failed, also trigger one explicitly. Tracing
81
// does not guarantee to give us a screenshot unless
82
// some driver action ran before.
83
if (persist) {
84
await this.takeScreenshot(name);
85
}
86
} catch (error) {
87
// Ignore
88
}
89
}
90
91
async didFinishLoad(): Promise<void> {
92
await this.whenLoaded;
93
}
94
95
private _cdpSession: playwright.CDPSession | undefined;
96
97
async startCDP() {
98
if (this._cdpSession) {
99
return;
100
}
101
102
this._cdpSession = await this.page.context().newCDPSession(this.page);
103
}
104
105
async collectGarbage() {
106
if (!this._cdpSession) {
107
throw new Error('CDP not started');
108
}
109
110
await this._cdpSession.send('HeapProfiler.collectGarbage');
111
}
112
113
async evaluate(options: Protocol.Runtime.evaluateParameters): Promise<Protocol.Runtime.evaluateReturnValue> {
114
if (!this._cdpSession) {
115
throw new Error('CDP not started');
116
}
117
118
return await this._cdpSession.send('Runtime.evaluate', options);
119
}
120
121
async releaseObjectGroup(parameters: Protocol.Runtime.releaseObjectGroupParameters): Promise<void> {
122
if (!this._cdpSession) {
123
throw new Error('CDP not started');
124
}
125
126
await this._cdpSession.send('Runtime.releaseObjectGroup', parameters);
127
}
128
129
async queryObjects(parameters: Protocol.Runtime.queryObjectsParameters): Promise<Protocol.Runtime.queryObjectsReturnValue> {
130
if (!this._cdpSession) {
131
throw new Error('CDP not started');
132
}
133
134
return await this._cdpSession.send('Runtime.queryObjects', parameters);
135
}
136
137
async callFunctionOn(parameters: Protocol.Runtime.callFunctionOnParameters): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
138
if (!this._cdpSession) {
139
throw new Error('CDP not started');
140
}
141
142
return await this._cdpSession.send('Runtime.callFunctionOn', parameters);
143
}
144
145
async takeHeapSnapshot(): Promise<string> {
146
if (!this._cdpSession) {
147
throw new Error('CDP not started');
148
}
149
150
let snapshot = '';
151
const listener = (c: { chunk: string }) => {
152
snapshot += c.chunk;
153
};
154
155
this._cdpSession.addListener('HeapProfiler.addHeapSnapshotChunk', listener);
156
157
await this._cdpSession.send('HeapProfiler.takeHeapSnapshot');
158
159
this._cdpSession.removeListener('HeapProfiler.addHeapSnapshotChunk', listener);
160
return snapshot;
161
}
162
163
async getProperties(parameters: Protocol.Runtime.getPropertiesParameters): Promise<Protocol.Runtime.getPropertiesReturnValue> {
164
if (!this._cdpSession) {
165
throw new Error('CDP not started');
166
}
167
168
return await this._cdpSession.send('Runtime.getProperties', parameters);
169
}
170
171
private async takeScreenshot(name: string): Promise<void> {
172
try {
173
const persistPath = join(this.options.logsPath, `playwright-screenshot-${PlaywrightDriver.screenShotCounter++}-${name.replace(/\s+/g, '-')}.png`);
174
175
await measureAndLog(() => this.page.screenshot({ path: persistPath, type: 'png' }), 'takeScreenshot', this.options.logger);
176
} catch (error) {
177
// Ignore
178
}
179
}
180
181
async reload() {
182
await this.page.reload();
183
}
184
185
async close() {
186
187
// Stop tracing
188
try {
189
if (this.options.tracing) {
190
await measureAndLog(() => this.context.tracing.stop(), 'stop tracing', this.options.logger);
191
}
192
} catch (error) {
193
// Ignore
194
}
195
196
// Web: Extract client logs
197
if (this.options.web) {
198
try {
199
await measureAndLog(() => this.saveWebClientLogs(), 'saveWebClientLogs()', this.options.logger);
200
} catch (error) {
201
this.options.logger.log(`Error saving web client logs (${error})`);
202
}
203
}
204
205
// exit via `close` method
206
try {
207
await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger);
208
} catch (error) {
209
this.options.logger.log(`Error closing application (${error})`);
210
}
211
212
// Server: via `teardown`
213
if (this.serverProcess) {
214
await measureAndLog(() => teardown(this.serverProcess!, this.options.logger), 'teardown server process', this.options.logger);
215
}
216
}
217
218
private async saveWebClientLogs(): Promise<void> {
219
const logs = await this.getLogs();
220
221
for (const log of logs) {
222
const absoluteLogsPath = join(this.options.logsPath, log.relativePath);
223
224
await promises.mkdir(dirname(absoluteLogsPath), { recursive: true });
225
await promises.writeFile(absoluteLogsPath, log.contents);
226
}
227
}
228
229
async sendKeybinding(keybinding: string, accept?: () => Promise<void> | void) {
230
const chords = keybinding.split(' ');
231
for (let i = 0; i < chords.length; i++) {
232
const chord = chords[i];
233
if (i > 0) {
234
await this.wait(100);
235
}
236
237
if (keybinding.startsWith('Alt') || keybinding.startsWith('Control') || keybinding.startsWith('Backspace')) {
238
await this.page.keyboard.press(keybinding);
239
return;
240
}
241
242
const keys = chord.split('+');
243
const keysDown: string[] = [];
244
for (let i = 0; i < keys.length; i++) {
245
if (keys[i] in PlaywrightDriver.vscodeToPlaywrightKey) {
246
keys[i] = PlaywrightDriver.vscodeToPlaywrightKey[keys[i]];
247
}
248
await this.page.keyboard.down(keys[i]);
249
keysDown.push(keys[i]);
250
}
251
while (keysDown.length > 0) {
252
await this.page.keyboard.up(keysDown.pop()!);
253
}
254
}
255
256
await accept?.();
257
}
258
259
async click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined) {
260
const { x, y } = await this.getElementXY(selector, xoffset, yoffset);
261
await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));
262
}
263
264
async setValue(selector: string, text: string) {
265
return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this.getDriverHandle(), selector, text] as const);
266
}
267
268
async getTitle() {
269
return this.page.title();
270
}
271
272
async isActiveElement(selector: string) {
273
return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this.getDriverHandle(), selector] as const);
274
}
275
276
async getElements(selector: string, recursive: boolean = false) {
277
return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this.getDriverHandle(), selector, recursive] as const);
278
}
279
280
async getElementXY(selector: string, xoffset?: number, yoffset?: number) {
281
return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this.getDriverHandle(), selector, xoffset, yoffset] as const);
282
}
283
284
async typeInEditor(selector: string, text: string) {
285
return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const);
286
}
287
288
async getEditorSelection(selector: string) {
289
return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const);
290
}
291
292
async getTerminalBuffer(selector: string) {
293
return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const);
294
}
295
296
async writeInTerminal(selector: string, text: string) {
297
return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this.getDriverHandle(), selector, text] as const);
298
}
299
300
async getLocaleInfo() {
301
return this.evaluateWithDriver(([driver]) => driver.getLocaleInfo());
302
}
303
304
async getLocalizedStrings() {
305
return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings());
306
}
307
308
async getLogs() {
309
return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const);
310
}
311
312
private async evaluateWithDriver<T>(pageFunction: PageFunction<IWindowDriver[], T>) {
313
return this.page.evaluate(pageFunction, [await this.getDriverHandle()]);
314
}
315
316
wait(ms: number): Promise<void> {
317
return wait(ms);
318
}
319
320
whenWorkbenchRestored(): Promise<void> {
321
return this.evaluateWithDriver(([driver]) => driver.whenWorkbenchRestored());
322
}
323
324
private async getDriverHandle(): Promise<playwright.JSHandle<IWindowDriver>> {
325
return this.page.evaluateHandle('window.driver');
326
}
327
328
async isAlive(): Promise<boolean> {
329
try {
330
await this.getDriverHandle();
331
return true;
332
} catch (error) {
333
return false;
334
}
335
}
336
}
337
338
export function wait(ms: number): Promise<void> {
339
return new Promise<void>(resolve => setTimeout(resolve, ms));
340
}
341
342