feat(trace): highlight strict mode violation elements in the snapshot

This commit is contained in:
Dmitry Gozman 2024-10-01 11:16:22 +01:00
parent 011034050b
commit ad3d9dd345
14 changed files with 49 additions and 95 deletions

View file

@ -77,10 +77,6 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
rawCallFunctionNoReply(func: Function, ...args: any[]) {
throw new Error('Method not implemented.');
}
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
const response = await this._session.send('script.callFunction', { const response = await this._session.send('script.callFunction', {
functionDeclaration, functionDeclaration,

View file

@ -53,16 +53,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return remoteObject.objectId!; return remoteObject.objectId!;
} }
rawCallFunctionNoReply(func: Function, ...args: any[]) {
this._client.send('Runtime.callFunctionOn', {
functionDeclaration: func.toString(),
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
returnByValue: true,
executionContextId: this._contextId,
userGesture: true
}).catch(() => {});
}
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: expression, functionDeclaration: expression,

View file

@ -421,7 +421,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return maybePoint; return maybePoint;
const point = roundPoint(maybePoint); const point = roundPoint(maybePoint);
progress.metadata.point = point; progress.metadata.point = point;
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined; let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;
if (force) { if (force) {
@ -490,9 +490,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done'; return 'done';
} }
private async _markAsTargetElement(metadata: CallMetadata) {
if (!metadata.id)
return;
await this.evaluateInUtility(([injected, node, callId]) => {
if (node.nodeType === 1 /* Node.ELEMENT_NODE */)
injected.markTargetElements(new Set([node as Node as Element]), callId);
}, metadata.id);
}
async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> { async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._hover(progress, options); const result = await this._hover(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -505,6 +515,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions = {}): Promise<void> { async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter }); const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter });
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -517,6 +528,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise<void> { async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._dblclick(progress, options); const result = await this._dblclick(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -529,6 +541,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions = {}): Promise<void> { async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._tap(progress, options); const result = await this._tap(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -541,6 +554,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> { async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._selectOption(progress, elements, values, options); const result = await this._selectOption(progress, elements, values, options);
return throwRetargetableDOMError(result); return throwRetargetableDOMError(result);
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -549,7 +563,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> { async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
let resultingOptions: string[] = []; let resultingOptions: string[] = [];
await this._retryAction(progress, 'select option', async () => { await this._retryAction(progress, 'select option', async () => {
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
if (!options.force) if (!options.force)
progress.log(` waiting for element to be visible and enabled`); progress.log(` waiting for element to be visible and enabled`);
const optionsToSelect = [...elements, ...values]; const optionsToSelect = [...elements, ...values];
@ -574,6 +588,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions = {}): Promise<void> { async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._fill(progress, value, options); const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result)); assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -582,7 +597,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> { async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> {
progress.log(` fill("${value}")`); progress.log(` fill("${value}")`);
return await this._retryAction(progress, 'fill', async () => { return await this._retryAction(progress, 'fill', async () => {
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
if (!options.force) if (!options.force)
progress.log(' waiting for element to be visible, enabled and editable'); progress.log(' waiting for element to be visible, enabled and editable');
const result = await this.evaluateInUtility(async ([injected, node, { value, force }]) => { const result = await this.evaluateInUtility(async ([injected, node, { value, force }]) => {
@ -629,6 +644,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const inputFileItems = await prepareFilesForUpload(this._frame, params); const inputFileItems = await prepareFilesForUpload(this._frame, params);
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._setInputFiles(progress, inputFileItems); const result = await this._setInputFiles(progress, inputFileItems);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(params)); }, this._page._timeoutSettings.timeout(params));
@ -655,7 +671,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (result === 'error:notconnected' || !result.asElement()) if (result === 'error:notconnected' || !result.asElement())
return 'error:notconnected'; return 'error:notconnected';
const retargeted = result.asElement() as ElementHandle<HTMLInputElement>; const retargeted = result.asElement() as ElementHandle<HTMLInputElement>;
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
if (localPaths || localDirectory) { if (localPaths || localDirectory) {
const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!; const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!;
@ -677,6 +693,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async focus(metadata: CallMetadata): Promise<void> { async focus(metadata: CallMetadata): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
await controller.run(async progress => { await controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._focus(progress); const result = await this._focus(progress);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, 0); }, 0);
@ -695,6 +712,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<void> { async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._type(progress, text, options); const result = await this._type(progress, text, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -702,7 +720,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _type(progress: Progress, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> { async _type(progress: Progress, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
progress.log(`elementHandle.type("${text}")`); progress.log(`elementHandle.type("${text}")`);
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
if (result !== 'done') if (result !== 'done')
return result; return result;
@ -714,6 +732,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async press(metadata: CallMetadata, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<void> { async press(metadata: CallMetadata, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
await this._markAsTargetElement(metadata);
const result = await this._press(progress, key, options); const result = await this._press(progress, key, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
@ -721,7 +740,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> { async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
progress.log(`elementHandle.press("${key}")`); progress.log(`elementHandle.press("${key}")`);
await progress.beforeInputAction(this); await this.instrumentation.onBeforeInputAction(this, progress.metadata);
return this._page._frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => { return this._page._frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => {
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
if (result !== 'done') if (result !== 'done')
@ -753,6 +772,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {}); const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
return throwRetargetableDOMError(result); return throwRetargetableDOMError(result);
}; };
await this._markAsTargetElement(progress.metadata);
if (await isChecked() === state) if (await isChecked() === state)
return 'done'; return 'done';
const result = await this._click(progress, { ...options, waitAfter: 'disabled' }); const result = await this._click(progress, { ...options, waitAfter: 'disabled' });

View file

@ -51,15 +51,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return payload.result!.objectId!; return payload.result!.objectId!;
} }
rawCallFunctionNoReply(func: Function, ...args: any[]) {
this._session.send('Runtime.callFunction', {
functionDeclaration: func.toString(),
args: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }) as any,
returnByValue: true,
executionContextId: this._executionContextId
}).catch(() => {});
}
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
const payload = await this._session.send('Runtime.callFunction', { const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: expression, functionDeclaration: expression,

View file

@ -1124,8 +1124,10 @@ export class Frame extends SdkObject {
progress.throwIfAborted(); progress.throwIfAborted();
if (!resolved) if (!resolved)
return continuePolling; return continuePolling;
const result = await resolved.injected.evaluateHandle((injected, { info }) => { const result = await resolved.injected.evaluateHandle((injected, { info, callId }) => {
const elements = injected.querySelectorAll(info.parsed, document); const elements = injected.querySelectorAll(info.parsed, document);
if (callId)
injected.markTargetElements(new Set(elements), callId);
const element = elements[0] as Element | undefined; const element = elements[0] as Element | undefined;
let log = ''; let log = '';
if (elements.length > 1) { if (elements.length > 1) {
@ -1136,7 +1138,7 @@ export class Frame extends SdkObject {
log = ` locator resolved to ${injected.previewNode(element)}`; log = ` locator resolved to ${injected.previewNode(element)}`;
} }
return { log, success: !!element, element }; return { log, success: !!element, element };
}, { info: resolved.info }); }, { info: resolved.info, callId: progress.metadata.id });
const { log, success } = await result.evaluate(r => ({ log: r.log, success: r.success })); const { log, success } = await result.evaluate(r => ({ log: r.log, success: r.success }));
if (log) if (log)
progress.log(log); progress.log(log);
@ -1478,6 +1480,8 @@ export class Frame extends SdkObject {
const { log, matches, received, missingReceived } = await injected.evaluate(async (injected, { info, options, callId }) => { const { log, matches, received, missingReceived } = await injected.evaluate(async (injected, { info, options, callId }) => {
const elements = info ? injected.querySelectorAll(info.parsed, document) : []; const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
if (callId)
injected.markTargetElements(new Set(elements), callId);
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array'); const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
let log = ''; let log = '';
if (isArray) if (isArray)
@ -1486,8 +1490,6 @@ export class Frame extends SdkObject {
throw injected.strictModeViolationError(info!.parsed, elements); throw injected.strictModeViolationError(info!.parsed, elements);
else if (elements.length) else if (elements.length)
log = ` locator resolved to ${injected.previewNode(elements[0])}`; log = ` locator resolved to ${injected.previewNode(elements[0])}`;
if (callId)
injected.markTargetElements(new Set(elements), callId);
return { log, ...await injected.expect(elements[0], options, elements) }; return { log, ...await injected.expect(elements[0], options, elements) };
}, { info, options, callId: progress.metadata.id }); }, { info, options, callId: progress.metadata.id });

View file

@ -20,7 +20,6 @@ import type { APIRequestContext } from './fetch';
import type { Browser } from './browser'; import type { Browser } from './browser';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType'; import type { BrowserType } from './browserType';
import type { ElementHandle } from './dom';
import type { Frame } from './frames'; import type { Frame } from './frames';
import type { Page } from './page'; import type { Page } from './page';
import type { Playwright } from './playwright'; import type { Playwright } from './playwright';
@ -57,7 +56,7 @@ export interface Instrumentation {
addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void; addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void;
removeListener(listener: InstrumentationListener): void; removeListener(listener: InstrumentationListener): void;
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>; onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onPageOpen(page: Page): void; onPageOpen(page: Page): void;
@ -70,7 +69,7 @@ export interface Instrumentation {
export interface InstrumentationListener { export interface InstrumentationListener {
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>; onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onPageOpen?(page: Page): void; onPageOpen?(page: Page): void;

View file

@ -53,7 +53,6 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
export interface ExecutionContextDelegate { export interface ExecutionContextDelegate {
rawEvaluateJSON(expression: string): Promise<any>; rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(expression: string): Promise<ObjectId>; rawEvaluateHandle(expression: string): Promise<ObjectId>;
rawCallFunctionNoReply(func: Function, ...args: any[]): void;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>; evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>; getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle; createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
@ -88,10 +87,6 @@ export class ExecutionContext extends SdkObject {
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(expression)); return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(expression));
} }
rawCallFunctionNoReply(func: Function, ...args: any[]): void {
this._delegate.rawCallFunctionNoReply(func, ...args);
}
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any> { evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any> {
return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds)); return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds));
} }
@ -151,10 +146,6 @@ export class JSHandle<T = any> extends SdkObject {
(globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle')); (globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle'));
} }
callFunctionNoReply(func: Function, arg: any) {
this._context.rawCallFunctionNoReply(func, this, arg);
}
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg?: Arg): Promise<R> { async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg?: Arg): Promise<R> {
return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg); return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
} }

View file

@ -18,7 +18,6 @@ import { TimeoutError } from './errors';
import { assert, monotonicTime } from '../utils'; import { assert, monotonicTime } from '../utils';
import type { LogName } from '../utils/debugLogger'; import type { LogName } from '../utils/debugLogger';
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation'; import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
import type { ElementHandle } from './dom';
import { ManualPromise } from '../utils/manualPromise'; import { ManualPromise } from '../utils/manualPromise';
export interface Progress { export interface Progress {
@ -27,7 +26,6 @@ export interface Progress {
isRunning(): boolean; isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void; cleanupWhenAborted(cleanup: () => any): void;
throwIfAborted(): void; throwIfAborted(): void;
beforeInputAction(element: ElementHandle): Promise<void>;
metadata: CallMetadata; metadata: CallMetadata;
} }
@ -89,9 +87,6 @@ export class ProgressController {
if (this._state === 'aborted') if (this._state === 'aborted')
throw new AbortedError(); throw new AbortedError();
}, },
beforeInputAction: async (element: ElementHandle) => {
await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata, element);
},
metadata: this.metadata metadata: this.metadata
}; };

View file

@ -24,7 +24,6 @@ import type { SnapshotData } from './snapshotterInjected';
import { frameSnapshotStreamer } from './snapshotterInjected'; import { frameSnapshotStreamer } from './snapshotterInjected';
import { calculateSha1, createGuid, monotonicTime } from '../../../utils'; import { calculateSha1, createGuid, monotonicTime } from '../../../utils';
import type { FrameSnapshot } from '@trace/snapshot'; import type { FrameSnapshot } from '@trace/snapshot';
import type { ElementHandle } from '../../dom';
import { mime } from '../../../utilsBundle'; import { mime } from '../../../utilsBundle';
export type SnapshotterBlob = { export type SnapshotterBlob = {
@ -105,21 +104,10 @@ export class Snapshotter {
eventsHelper.removeEventListeners(this._eventListeners); eventsHelper.removeEventListeners(this._eventListeners);
} }
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<void> { async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise<void> {
// Prepare expression synchronously. // Prepare expression synchronously.
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`; const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
// In a best-effort manner, without waiting for it, mark target element.
element?.callFunctionNoReply((element: Element, callId: string) => {
const customEvent = new CustomEvent('__playwright_target__', {
bubbles: true,
cancelable: true,
detail: callId,
composed: true,
});
element.dispatchEvent(customEvent);
}, callId);
// In each frame, in a non-stalling manner, capture the snapshots. // In each frame, in a non-stalling manner, capture the snapshots.
const snapshots = page.frames().map(async frame => { const snapshots = page.frames().map(async frame => {
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData; const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;

View file

@ -23,7 +23,6 @@ import { commandsWithTracingSnapshots } from '../../../protocol/debug';
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils'; import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
import { Artifact } from '../../artifact'; import { Artifact } from '../../artifact';
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import type { ElementHandle } from '../../dom';
import type { APIRequestContext } from '../../fetch'; import type { APIRequestContext } from '../../fetch';
import type { CallMetadata, InstrumentationListener } from '../../instrumentation'; import type { CallMetadata, InstrumentationListener } from '../../instrumentation';
import { SdkObject } from '../../instrumentation'; import { SdkObject } from '../../instrumentation';
@ -341,7 +340,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { artifact }; return { artifact };
} }
async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> { async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!this._snapshotter) if (!this._snapshotter)
return; return;
if (!sdkObject.attribution.page) if (!sdkObject.attribution.page)
@ -350,7 +349,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return; return;
if (!shouldCaptureSnapshot(metadata)) if (!shouldCaptureSnapshot(metadata))
return; return;
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName, element).catch(() => {}); await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {});
} }
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
@ -365,7 +364,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata); return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
} }
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) { onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
if (!this._state?.callIds.has(metadata.id)) if (!this._state?.callIds.has(metadata.id))
return Promise.resolve(); return Promise.resolve();
// IMPORTANT: no awaits before this._appendTraceEvent in this method. // IMPORTANT: no awaits before this._appendTraceEvent in this method.
@ -375,7 +374,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling(); sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
event.inputSnapshot = `input@${metadata.id}`; event.inputSnapshot = `input@${metadata.id}`;
this._appendTraceEvent(event); this._appendTraceEvent(event);
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata, element); return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata);
} }
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) { onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) {

View file

@ -21,7 +21,6 @@ import type { SnapshotRenderer } from '../../../../../trace-viewer/src/sw/snapsh
import { SnapshotStorage } from '../../../../../trace-viewer/src/sw/snapshotStorage'; import { SnapshotStorage } from '../../../../../trace-viewer/src/sw/snapshotStorage';
import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter'; import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter';
import { Snapshotter } from '../recorder/snapshotter'; import { Snapshotter } from '../recorder/snapshotter';
import type { ElementHandle } from '../../dom';
import type { HarTracerDelegate } from '../../har/harTracer'; import type { HarTracerDelegate } from '../../har/harTracer';
import { HarTracer } from '../../har/harTracer'; import { HarTracer } from '../../har/harTracer';
import type * as har from '@trace/har'; import type * as har from '@trace/har';
@ -59,11 +58,11 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
this._harTracer.stop(); this._harTracer.stop();
} }
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> { async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise<SnapshotRenderer> {
if (this._snapshotReadyPromises.has(snapshotName)) if (this._snapshotReadyPromises.has(snapshotName))
throw new Error('Duplicate snapshot name: ' + snapshotName); throw new Error('Duplicate snapshot name: ' + snapshotName);
this._snapshotter.captureSnapshot(page, callId, snapshotName, element).catch(() => {}); this._snapshotter.captureSnapshot(page, callId, snapshotName).catch(() => {});
const promise = new ManualPromise<SnapshotRenderer>(); const promise = new ManualPromise<SnapshotRenderer>();
this._snapshotReadyPromises.set(snapshotName, promise); this._snapshotReadyPromises.set(snapshotName, promise);
return promise; return promise;

View file

@ -60,16 +60,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} }
} }
rawCallFunctionNoReply(func: Function, ...args: any[]) {
this._session.send('Runtime.callFunctionOn', {
functionDeclaration: func.toString(),
objectId: args.find(a => a instanceof js.JSHandle)!._objectId!,
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
returnByValue: true,
emulateUserGesture: true
}).catch(() => {});
}
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
try { try {
const response = await this._session.send('Runtime.callFunctionOn', { const response = await this._session.send('Runtime.callFunctionOn', {

View file

@ -215,20 +215,6 @@ it.describe('snapshots', () => {
} }
}); });
it('should capture snapshot target', async ({ page, toImpl, snapshotter }) => {
await page.setContent('<button>Hello</button><button>World</button>');
{
const handle = await page.$('text=Hello');
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1', toImpl(handle));
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON>World</BUTTON>');
}
{
const handle = await page.$('text=World');
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2', toImpl(handle));
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON __playwright_target__=\"call@2\">World</BUTTON>');
}
});
it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => { it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => {
await page.setContent('<button>Hello</button>'); await page.setContent('<button>Hello</button>');
{ {

View file

@ -776,6 +776,8 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
await expect(page.locator('text=t6')).toHaveText(/t6/i); await expect(page.locator('text=t6')).toHaveText(/t6/i);
await expect(page.locator('text=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {}); await expect(page.locator('text=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
await page.mouse.move(123, 234); await page.mouse.move(123, 234);
await page.getByText(/^t\d$/).click().catch(() => {});
await expect(page.getByText(/t3|t4/)).toBeVisible().catch(() => {});
}); });
async function highlightedDivs(frameLocator: FrameLocator) { async function highlightedDivs(frameLocator: FrameLocator) {
@ -817,6 +819,12 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
const frameMouseMove = await traceViewer.snapshotFrame('mouse.move'); const frameMouseMove = await traceViewer.snapshotFrame('mouse.move');
await expect(frameMouseMove.locator('x-pw-pointer')).toBeVisible(); await expect(frameMouseMove.locator('x-pw-pointer')).toBeVisible();
const frameClickStrictViolation = await traceViewer.snapshotFrame('locator.click');
await expect.poll(() => highlightedDivs(frameClickStrictViolation)).toEqual(['t1', 't2', 't3', 't4', 't5', 't6']);
const frameExpectStrictViolation = await traceViewer.snapshotFrame('expect.toBeVisible');
await expect.poll(() => highlightedDivs(frameExpectStrictViolation)).toEqual(['t3', 't4']);
}); });
test('should highlight target element in shadow dom', async ({ page, server, runAndTrace }) => { test('should highlight target element in shadow dom', async ({ page, server, runAndTrace }) => {