Skip to content

Commit 26dee18

Browse files
committed
feat(trace): add action screenshot and aria snapshot events
1 parent c873099 commit 26dee18

19 files changed

Lines changed: 288 additions & 106 deletions

File tree

docs/src/api/class-tracing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ a timeline preview.
148148

149149
### option: Tracing.start.snapshots
150150
* since: v1.12
151+
* langs: js
152+
- `snapshots` <[boolean]|[Object]>
153+
- `dom` ?<[boolean]> Capture DOM snapshot on every action and record network activity. Optional.
154+
- `aria` ?<[boolean]> Capture aria snapshot of the page on every action. Optional.
155+
- `screen` ?<[boolean]> Capture a screenshot of the page on every action. Optional.
156+
157+
Which snapshots to capture on every action. Passing `true` is a shortcut for `{ dom: true }`.
158+
159+
### option: Tracing.start.snapshots
160+
* since: v1.12
161+
* langs: java, python, csharp
151162
- `snapshots` <[boolean]>
152163

153164
If this option is true tracing will

packages/playwright-client/types/types.d.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23692,11 +23692,24 @@ export interface Tracing {
2369223692
screenshots?: boolean;
2369323693

2369423694
/**
23695-
* If this option is true tracing will
23696-
* - capture DOM snapshot on every action
23697-
* - record network activity
23695+
* Which snapshots to capture on every action. Passing `true` is a shortcut for `{ dom: true }`.
2369823696
*/
23699-
snapshots?: boolean;
23697+
snapshots?: boolean|{
23698+
/**
23699+
* Capture DOM snapshot on every action and record network activity. Optional.
23700+
*/
23701+
dom?: boolean;
23702+
23703+
/**
23704+
* Capture aria snapshot of the page on every action. Optional.
23705+
*/
23706+
aria?: boolean;
23707+
23708+
/**
23709+
* Capture a screenshot of the page on every action. Optional.
23710+
*/
23711+
screen?: boolean;
23712+
};
2370023713

2370123714
/**
2370223715
* Whether to include source files for trace actions.

packages/playwright-core/src/client/channels.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5047,14 +5047,18 @@ export interface TracingChannel extends TracingEventTarget, Channel {
50475047
}
50485048
export type TracingTracingStartParams = {
50495049
name?: string,
5050-
snapshots?: boolean,
5051-
screenshots?: boolean,
5050+
snapshotDom?: boolean,
5051+
snapshotAria?: boolean,
5052+
snapshotScreen?: boolean,
5053+
screencast?: boolean,
50525054
live?: boolean,
50535055
};
50545056
export type TracingTracingStartOptions = {
50555057
name?: string,
5056-
snapshots?: boolean,
5057-
screenshots?: boolean,
5058+
snapshotDom?: boolean,
5059+
snapshotAria?: boolean,
5060+
snapshotScreen?: boolean,
5061+
screencast?: boolean,
50585062
live?: boolean,
50595063
};
50605064
export type TracingTracingStartResult = void;

packages/playwright-core/src/client/tracing.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,17 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
4242
super(parent, type, guid, initializer);
4343
}
4444

45-
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, live?: boolean } = {}) {
45+
async start(options: { name?: string, title?: string, snapshots?: boolean | { dom?: boolean, aria?: boolean, screen?: boolean }, screenshots?: boolean, sources?: boolean, live?: boolean } = {}) {
4646
await this._wrapApiCall(async () => {
4747
this._includeSources = !!options.sources;
4848
this._isLive = !!options.live;
49+
const snapshots = typeof options.snapshots === 'object' ? options.snapshots : { dom: options.snapshots };
4950
await this._channel.tracingStart({
5051
name: options.name,
51-
snapshots: options.snapshots,
52-
screenshots: options.screenshots,
52+
snapshotDom: snapshots.dom,
53+
snapshotAria: snapshots.aria,
54+
snapshotScreen: snapshots.screen,
55+
screencast: options.screenshots,
5356
live: options.live,
5457
}, kNoTimeout);
5558
const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title }, kNoTimeout);

packages/playwright-core/src/server/bidi/bidiInput.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class RawKeyboardImpl implements input.RawKeyboard {
6060
return;
6161
}
6262
try {
63-
frame = await progress.race(element.contentFrame(progress));
63+
frame = await element.contentFrame(progress);
6464
} finally {
6565
element.dispose();
6666
}

packages/playwright-core/src/server/channels.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5048,14 +5048,18 @@ export interface TracingChannel extends TracingEventTarget, Channel {
50485048
}
50495049
export type TracingTracingStartParams = {
50505050
name?: string,
5051-
snapshots?: boolean,
5052-
screenshots?: boolean,
5051+
snapshotDom?: boolean,
5052+
snapshotAria?: boolean,
5053+
snapshotScreen?: boolean,
5054+
screencast?: boolean,
50535055
live?: boolean,
50545056
};
50555057
export type TracingTracingStartOptions = {
50565058
name?: string,
5057-
snapshots?: boolean,
5058-
screenshots?: boolean,
5059+
snapshotDom?: boolean,
5060+
snapshotAria?: boolean,
5061+
snapshotScreen?: boolean,
5062+
screencast?: boolean,
50595063
live?: boolean,
50605064
};
50615065
export type TracingTracingStartResult = void;

packages/playwright-core/src/server/debugger.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ export class Debugger extends SdkObject<DebuggerEventMap> implements Instrumenta
117117
this._muted = muted;
118118
}
119119

120-
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
120+
async onBeforeCall(progress: Progress, sdkObject: SdkObject): Promise<void> {
121+
const { metadata } = progress;
121122
if (!metadata.internal && metadata.method)
122123
this._ongoingCalls.set(metadata.id, { metadata, sentLogCount: 0, status: 'running' });
123124
if (this._apiCallsEnabled) {
@@ -131,10 +132,11 @@ export class Debugger extends SdkObject<DebuggerEventMap> implements Instrumenta
131132
const pauseBeforeAction = !!this._pauseAt.next && !!metainfo?.pause && (this._pauseBeforeWaitingActions || !metainfo?.isAutoWaiting);
132133
const pauseOnLocation = !!this._pauseAt.location && matchesLocation(metadata, this._pauseAt.location);
133134
if (pauseOnPauseCall || pauseBeforeAction || pauseOnLocation)
134-
await this._pause(sdkObject, metadata);
135+
await this._pause(progress, sdkObject);
135136
}
136137

137-
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, point?: Point): Promise<void> {
138+
async onBeforeInputAction(progress: Progress, sdkObject: SdkObject, point?: Point): Promise<void> {
139+
const { metadata } = progress;
138140
const call = this._ongoingCalls.get(metadata.id);
139141
if (call) {
140142
call.actionPoint = point;
@@ -148,10 +150,11 @@ export class Debugger extends SdkObject<DebuggerEventMap> implements Instrumenta
148150
const metainfo = getMetainfo(metadata);
149151
const pauseBeforeInput = !!this._pauseAt.next && !!metainfo?.pause && !!metainfo?.isAutoWaiting && !this._pauseBeforeWaitingActions;
150152
if (pauseBeforeInput)
151-
await this._pause(sdkObject, metadata);
153+
await this._pause(progress, sdkObject);
152154
}
153155

154-
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
156+
async onAfterCall(progress: Progress, sdkObject: SdkObject): Promise<void> {
157+
const { metadata } = progress;
155158
const call = this._ongoingCalls.get(metadata.id);
156159
if (!call)
157160
return;
@@ -212,7 +215,8 @@ export class Debugger extends SdkObject<DebuggerEventMap> implements Instrumenta
212215
this.emit(Debugger.Events.ApiCallsUpdated, updates);
213216
}
214217

215-
private async _pause(sdkObject: SdkObject, metadata: CallMetadata) {
218+
private async _pause(progress: Progress, sdkObject: SdkObject) {
219+
const { metadata } = progress;
216220
if (this._muted || metadata.internal)
217221
return;
218222
if (this._pausedCall)

packages/playwright-core/src/server/dispatchers/dispatcher.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -352,16 +352,21 @@ export class DispatcherConnection {
352352
log: [],
353353
};
354354

355-
const controller = dispatcher.createProgressController(callMetadata);
356-
this._activeProgressControllers.set(callMetadata.id, controller);
355+
const beforeController = dispatcher.createProgressController(callMetadata);
356+
this._activeProgressControllers.set(callMetadata.id, beforeController);
357+
// Be generous with the tracing timeout in case it wants to capture a screenshot, fail silently.
358+
await beforeController.run(progress => sdkObject.instrumentation.onBeforeCall(progress, sdkObject), 3000).catch(() => {});
359+
this._activeProgressControllers.delete(callMetadata.id);
357360

358-
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
359361
const response: any = { id };
360362
try {
361363
// If the dispatcher has been disposed while running the instrumentation call, error out.
362364
if (this._dispatcherByGuid.get(guid) !== dispatcher)
363365
throw new TargetClosedError(sdkObject.closeReason());
366+
const controller = dispatcher.createProgressController(callMetadata);
367+
this._activeProgressControllers.set(callMetadata.id, controller);
364368
const result = await controller.run(progress => (dispatcher as any)[method](validParams, progress), validMetadata.timeout);
369+
this._activeProgressControllers.delete(callMetadata.id);
365370
const validator = findValidator(dispatcher._type, method, 'Result');
366371
response.result = validator(result, '', this._validatorToWireContext());
367372
callMetadata.result = result;
@@ -384,7 +389,10 @@ export class DispatcherConnection {
384389
callMetadata.error = response.error;
385390
} finally {
386391
callMetadata.endTime = monotonicTime();
387-
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
392+
const afterController = dispatcher.createProgressController(callMetadata);
393+
this._activeProgressControllers.set(callMetadata.id, afterController);
394+
// Be generous with the tracing timeout in case it wants to capture a screenshot, fail silently.
395+
await afterController.run(progress => sdkObject.instrumentation.onAfterCall(progress, sdkObject), 3000).catch(() => {});
388396
if (metainfo?.slowMo)
389397
await this._doSlowMo(sdkObject);
390398
this._activeProgressControllers.delete(callMetadata.id);
@@ -432,7 +440,8 @@ export class DispatcherConnection {
432440
log: [],
433441
};
434442
this._waitOperations.set(info.waitId, callMetadata);
435-
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata).catch(() => {});
443+
const controller = ProgressController.createForSdkObject(sdkObject, callMetadata);
444+
await controller.run(progress => sdkObject.instrumentation.onBeforeCall(progress, sdkObject).catch(() => {}));
436445
return;
437446
}
438447

@@ -448,7 +457,8 @@ export class DispatcherConnection {
448457
originalMetadata.endTime = monotonicTime();
449458
originalMetadata.error = info.error ? { error: { name: 'Error', message: info.error } } : undefined;
450459
this._waitOperations.delete(info.waitId);
451-
await sdkObject.instrumentation.onAfterCall(sdkObject, originalMetadata).catch(() => {});
460+
const controller = ProgressController.createForSdkObject(sdkObject, originalMetadata);
461+
await controller.run(progress => sdkObject.instrumentation.onAfterCall(progress, sdkObject).catch(() => {}));
452462
}
453463
}
454464
}

packages/playwright-core/src/server/dom.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
454454
if (typeof maybeResult === 'string')
455455
return maybeResult;
456456
const point = roundPoint(maybeResult.point);
457-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata, point, maybeResult.box));
457+
await this.instrumentation.onBeforeInputAction(progress, this, point, maybeResult.box);
458458

459459
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;
460460
if (force) {
@@ -577,7 +577,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
577577
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
578578
let resultingOptions: string[] = [];
579579
const result = await this._retryAction(progress, 'select option', async progress => {
580-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
580+
await this.instrumentation.onBeforeInputAction(progress, this);
581581
if (!options.force)
582582
progress.log(` waiting for element to be visible and enabled`);
583583
const optionsToSelect = [...elements, ...values];
@@ -610,7 +610,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
610610
async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> {
611611
progress.log(` fill("${value}")`);
612612
return await this._retryAction(progress, 'fill', async progress => {
613-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
613+
await this.instrumentation.onBeforeInputAction(progress, this);
614614
if (!options.force)
615615
progress.log(' waiting for element to be visible, enabled and editable');
616616
const result = await progress.race(this.evaluateInUtility(async ([injected, node, { value, force }]) => {
@@ -744,7 +744,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
744744
if (result === 'error:notconnected' || !result.asElement())
745745
return 'error:notconnected';
746746
const retargeted = result.asElement() as ElementHandle<HTMLInputElement>;
747-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
747+
await this.instrumentation.onBeforeInputAction(progress, this);
748748
if (localPaths || localDirectory) {
749749
const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!;
750750
await progress.race(Promise.all((localPathsOrDirectory).map(localPath => (
@@ -785,7 +785,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
785785

786786
async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
787787
progress.log(`elementHandle.type("${text}")`);
788-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
788+
await this.instrumentation.onBeforeInputAction(progress, this);
789789
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
790790
if (result !== 'done')
791791
return result;
@@ -801,7 +801,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
801801

802802
async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
803803
progress.log(`elementHandle.press("${key}")`);
804-
await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata));
804+
await this.instrumentation.onBeforeInputAction(progress, this);
805805
return this._page.frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async progress => {
806806
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
807807
if (result !== 'done')

packages/playwright-core/src/server/input.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class Keyboard {
5454
}
5555

5656
async apiDown(progress: Progress, key: string) {
57-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
57+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
5858
await this.down(progress, key);
5959
}
6060

@@ -82,7 +82,7 @@ export class Keyboard {
8282
}
8383

8484
async apiUp(progress: Progress, key: string) {
85-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
85+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
8686
await this.up(progress, key);
8787
}
8888

@@ -95,7 +95,7 @@ export class Keyboard {
9595
}
9696

9797
async apiInsertText(progress: Progress, text: string) {
98-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
98+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
9999
await this.insertText(progress, text);
100100
}
101101

@@ -104,7 +104,7 @@ export class Keyboard {
104104
}
105105

106106
async apiType(progress: Progress, text: string, options?: { delay?: number }) {
107-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
107+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
108108
await this.type(progress, text, options);
109109
}
110110

@@ -122,7 +122,7 @@ export class Keyboard {
122122
}
123123

124124
async apiPress(progress: Progress, key: string, options: { delay?: number } = {}) {
125-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
125+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
126126
await this.press(progress, key, options);
127127
}
128128

@@ -214,7 +214,7 @@ export class Mouse {
214214
}
215215

216216
async apiMove(progress: Progress, x: number, y: number, options: { steps?: number, forClick?: boolean } = {}) {
217-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata, { x, y }));
217+
await this._page.instrumentation.onBeforeInputAction(progress, this._page, { x, y });
218218
await this.move(progress, x, y, options);
219219
}
220220

@@ -232,7 +232,7 @@ export class Mouse {
232232
}
233233

234234
async apiDown(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) {
235-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata, this._currentPoint()));
235+
await this._page.instrumentation.onBeforeInputAction(progress, this._page, this._currentPoint());
236236
await this.down(progress, options);
237237
}
238238

@@ -244,7 +244,7 @@ export class Mouse {
244244
}
245245

246246
async apiUp(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) {
247-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata, this._currentPoint()));
247+
await this._page.instrumentation.onBeforeInputAction(progress, this._page, this._currentPoint());
248248
await this.up(progress, options);
249249
}
250250

@@ -256,7 +256,7 @@ export class Mouse {
256256
}
257257

258258
async apiClick(progress: Progress, x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number, steps?: number } = {}) {
259-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata, { x, y }));
259+
await this._page.instrumentation.onBeforeInputAction(progress, this._page, { x, y });
260260
await this.click(progress, x, y, options);
261261
}
262262

@@ -289,7 +289,7 @@ export class Mouse {
289289
}
290290

291291
async apiWheel(progress: Progress, deltaX: number, deltaY: number) {
292-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata));
292+
await this._page.instrumentation.onBeforeInputAction(progress, this._page);
293293
await this._raw.wheel(progress, this._x, this._y, this._buttons, this._keyboard._modifiers(), deltaX, deltaY);
294294
}
295295
}
@@ -370,7 +370,7 @@ export class Touchscreen {
370370
async apiTap(progress: Progress, x: number, y: number) {
371371
if (!this._page.browserContext._options.hasTouch)
372372
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
373-
await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata, { x, y }));
373+
await this._page.instrumentation.onBeforeInputAction(progress, this._page, { x, y });
374374
await this.tap(progress, x, y);
375375
}
376376

0 commit comments

Comments
 (0)