mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-01 08:34:02 +03:00
feat(trace): highlight strict mode violation elements in the snapshot (#32893)
This is fixing a case where the test failed with strict mode violation, but all the matched elements are not highlighted in the trace. For example, all the buttons will be highlighted when the following line fails due to strict mode violation: ```ts await page.locator('button').click(); ``` To achieve this, we mark elements during `querySelector` phase instead of inside `onBeforeInputAction`. This allows us to only mark from inside the `InjectedScript` and remove the other way of marking from inside the `Snapshotter`.
This commit is contained in:
parent
daac0ddd24
commit
773202867d
@ -77,10 +77,6 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||
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> {
|
||||
const response = await this._session.send('script.callFunction', {
|
||||
functionDeclaration,
|
||||
|
@ -53,16 +53,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||
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> {
|
||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: expression,
|
||||
|
@ -421,7 +421,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return maybePoint;
|
||||
const point = roundPoint(maybePoint);
|
||||
progress.metadata.point = point;
|
||||
await progress.beforeInputAction(this);
|
||||
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||
|
||||
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;
|
||||
if (force) {
|
||||
@ -490,9 +490,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._hover(progress, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter });
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._dblclick(progress, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._tap(progress, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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[]> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._selectOption(progress, elements, values, options);
|
||||
return throwRetargetableDOMError(result);
|
||||
}, 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'> {
|
||||
let resultingOptions: string[] = [];
|
||||
await this._retryAction(progress, 'select option', async () => {
|
||||
await progress.beforeInputAction(this);
|
||||
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||
if (!options.force)
|
||||
progress.log(` waiting for element to be visible and enabled`);
|
||||
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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._fill(progress, value, options);
|
||||
assertDone(throwRetargetableDOMError(result));
|
||||
}, 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'> {
|
||||
progress.log(` fill("${value}")`);
|
||||
return await this._retryAction(progress, 'fill', async () => {
|
||||
await progress.beforeInputAction(this);
|
||||
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||
if (!options.force)
|
||||
progress.log(' waiting for element to be visible, enabled and editable');
|
||||
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 controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._setInputFiles(progress, inputFileItems);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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())
|
||||
return 'error:notconnected';
|
||||
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.
|
||||
if (localPaths || localDirectory) {
|
||||
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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
await controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._focus(progress);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._type(progress, text, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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'> {
|
||||
progress.log(`elementHandle.type("${text}")`);
|
||||
await progress.beforeInputAction(this);
|
||||
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
||||
if (result !== 'done')
|
||||
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> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
await this._markAsTargetElement(metadata);
|
||||
const result = await this._press(progress, key, options);
|
||||
return assertDone(throwRetargetableDOMError(result));
|
||||
}, 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'> {
|
||||
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 () => {
|
||||
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
||||
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'), {});
|
||||
return throwRetargetableDOMError(result);
|
||||
};
|
||||
await this._markAsTargetElement(progress.metadata);
|
||||
if (await isChecked() === state)
|
||||
return 'done';
|
||||
const result = await this._click(progress, { ...options, waitAfter: 'disabled' });
|
||||
|
@ -51,15 +51,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||
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> {
|
||||
const payload = await this._session.send('Runtime.callFunction', {
|
||||
functionDeclaration: expression,
|
||||
|
@ -1124,8 +1124,10 @@ export class Frame extends SdkObject {
|
||||
progress.throwIfAborted();
|
||||
if (!resolved)
|
||||
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);
|
||||
if (callId)
|
||||
injected.markTargetElements(new Set(elements), callId);
|
||||
const element = elements[0] as Element | undefined;
|
||||
let log = '';
|
||||
if (elements.length > 1) {
|
||||
@ -1136,7 +1138,7 @@ export class Frame extends SdkObject {
|
||||
log = ` locator resolved to ${injected.previewNode(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 }));
|
||||
if (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 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');
|
||||
let log = '';
|
||||
if (isArray)
|
||||
@ -1486,8 +1490,6 @@ export class Frame extends SdkObject {
|
||||
throw injected.strictModeViolationError(info!.parsed, elements);
|
||||
else if (elements.length)
|
||||
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) };
|
||||
}, { info, options, callId: progress.metadata.id });
|
||||
|
||||
|
@ -20,7 +20,6 @@ import type { APIRequestContext } from './fetch';
|
||||
import type { Browser } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { BrowserType } from './browserType';
|
||||
import type { ElementHandle } from './dom';
|
||||
import type { Frame } from './frames';
|
||||
import type { Page } from './page';
|
||||
import type { Playwright } from './playwright';
|
||||
@ -57,7 +56,7 @@ export interface Instrumentation {
|
||||
addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void;
|
||||
removeListener(listener: InstrumentationListener): 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;
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onPageOpen(page: Page): void;
|
||||
@ -70,7 +69,7 @@ export interface Instrumentation {
|
||||
|
||||
export interface InstrumentationListener {
|
||||
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;
|
||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onPageOpen?(page: Page): void;
|
||||
|
@ -53,7 +53,6 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
|
||||
export interface ExecutionContextDelegate {
|
||||
rawEvaluateJSON(expression: string): Promise<any>;
|
||||
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>;
|
||||
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
|
||||
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
||||
@ -88,10 +87,6 @@ export class ExecutionContext extends SdkObject {
|
||||
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> {
|
||||
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'));
|
||||
}
|
||||
|
||||
callFunctionNoReply(func: Function, arg: any) {
|
||||
this._context.rawCallFunctionNoReply(func, this, arg);
|
||||
}
|
||||
|
||||
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg?: Arg): Promise<R> {
|
||||
return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import { TimeoutError } from './errors';
|
||||
import { assert, monotonicTime } from '../utils';
|
||||
import type { LogName } from '../utils/debugLogger';
|
||||
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||
import type { ElementHandle } from './dom';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
export interface Progress {
|
||||
@ -27,7 +26,6 @@ export interface Progress {
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
beforeInputAction(element: ElementHandle): Promise<void>;
|
||||
metadata: CallMetadata;
|
||||
}
|
||||
|
||||
@ -89,9 +87,6 @@ export class ProgressController {
|
||||
if (this._state === 'aborted')
|
||||
throw new AbortedError();
|
||||
},
|
||||
beforeInputAction: async (element: ElementHandle) => {
|
||||
await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata, element);
|
||||
},
|
||||
metadata: this.metadata
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,6 @@ import type { SnapshotData } from './snapshotterInjected';
|
||||
import { frameSnapshotStreamer } from './snapshotterInjected';
|
||||
import { calculateSha1, createGuid, monotonicTime } from '../../../utils';
|
||||
import type { FrameSnapshot } from '@trace/snapshot';
|
||||
import type { ElementHandle } from '../../dom';
|
||||
import { mime } from '../../../utilsBundle';
|
||||
|
||||
export type SnapshotterBlob = {
|
||||
@ -105,21 +104,10 @@ export class Snapshotter {
|
||||
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.
|
||||
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.
|
||||
const snapshots = page.frames().map(async frame => {
|
||||
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;
|
||||
|
@ -23,7 +23,6 @@ import { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
|
||||
import { Artifact } from '../../artifact';
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
import type { ElementHandle } from '../../dom';
|
||||
import type { APIRequestContext } from '../../fetch';
|
||||
import type { CallMetadata, InstrumentationListener } from '../../instrumentation';
|
||||
import { SdkObject } from '../../instrumentation';
|
||||
@ -341,7 +340,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
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)
|
||||
return;
|
||||
if (!sdkObject.attribution.page)
|
||||
@ -350,7 +349,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
return;
|
||||
if (!shouldCaptureSnapshot(metadata))
|
||||
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) {
|
||||
@ -365,7 +364,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
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))
|
||||
return Promise.resolve();
|
||||
// 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();
|
||||
event.inputSnapshot = `input@${metadata.id}`;
|
||||
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) {
|
||||
|
@ -21,7 +21,6 @@ import type { SnapshotRenderer } from '../../../../../trace-viewer/src/sw/snapsh
|
||||
import { SnapshotStorage } from '../../../../../trace-viewer/src/sw/snapshotStorage';
|
||||
import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter';
|
||||
import { Snapshotter } from '../recorder/snapshotter';
|
||||
import type { ElementHandle } from '../../dom';
|
||||
import type { HarTracerDelegate } from '../../har/harTracer';
|
||||
import { HarTracer } from '../../har/harTracer';
|
||||
import type * as har from '@trace/har';
|
||||
@ -59,11 +58,11 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
||||
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))
|
||||
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>();
|
||||
this._snapshotReadyPromises.set(snapshotName, promise);
|
||||
return promise;
|
||||
|
@ -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> {
|
||||
try {
|
||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||
|
@ -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 }) => {
|
||||
await page.setContent('<button>Hello</button>');
|
||||
{
|
||||
|
@ -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=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
|
||||
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) {
|
||||
@ -817,6 +819,12 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
|
||||
|
||||
const frameMouseMove = await traceViewer.snapshotFrame('mouse.move');
|
||||
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 }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user