From 773202867d90ddd1a6aa9a65c044793c39daa87d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 2 Oct 2024 00:00:45 -0700 Subject: [PATCH] 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`. --- .../src/server/bidi/bidiExecutionContext.ts | 4 --- .../src/server/chromium/crExecutionContext.ts | 10 ------ packages/playwright-core/src/server/dom.ts | 32 +++++++++++++++---- .../src/server/firefox/ffExecutionContext.ts | 9 ------ packages/playwright-core/src/server/frames.ts | 10 +++--- .../src/server/instrumentation.ts | 5 ++- .../playwright-core/src/server/javascript.ts | 9 ------ .../playwright-core/src/server/progress.ts | 5 --- .../src/server/trace/recorder/snapshotter.ts | 14 +------- .../src/server/trace/recorder/tracing.ts | 9 +++--- .../server/trace/test/inMemorySnapshotter.ts | 5 ++- .../src/server/webkit/wkExecutionContext.ts | 10 ------ tests/library/snapshotter.spec.ts | 14 -------- tests/library/trace-viewer.spec.ts | 8 +++++ 14 files changed, 49 insertions(+), 95 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts index 201b3f999d..f53b160ccf 100644 --- a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts +++ b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts @@ -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, values: any[], objectIds: string[]): Promise { const response = await this._session.send('script.callFunction', { functionDeclaration, diff --git a/packages/playwright-core/src/server/chromium/crExecutionContext.ts b/packages/playwright-core/src/server/chromium/crExecutionContext.ts index d54505d283..1cd58de7af 100644 --- a/packages/playwright-core/src/server/chromium/crExecutionContext.ts +++ b/packages/playwright-core/src/server/chromium/crExecutionContext.ts @@ -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, values: any[], objectIds: string[]): Promise { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { functionDeclaration: expression, diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 05a8b4fda2..c2ed979fbe 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -421,7 +421,7 @@ export class ElementHandle extends js.JSHandle { 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 | undefined; if (force) { @@ -490,9 +490,19 @@ export class ElementHandle extends js.JSHandle { 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 { 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 extends js.JSHandle { async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions = {}): Promise { 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 extends js.JSHandle { async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise { 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 extends js.JSHandle { async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions = {}): Promise { 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 extends js.JSHandle { async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { 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 extends js.JSHandle { async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { 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 extends js.JSHandle { async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions = {}): Promise { 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 extends js.JSHandle { 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 extends js.JSHandle { 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 extends js.JSHandle { if (result === 'error:notconnected' || !result.asElement()) return 'error:notconnected'; const retargeted = result.asElement() as ElementHandle; - 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 extends js.JSHandle { async focus(metadata: CallMetadata): Promise { 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 extends js.JSHandle { async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise { 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 extends js.JSHandle { 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 extends js.JSHandle { async press(metadata: CallMetadata, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise { 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 extends js.JSHandle { 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 extends js.JSHandle { 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' }); diff --git a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts index ba981da942..c7a3f106f8 100644 --- a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts +++ b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts @@ -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, values: any[], objectIds: string[]): Promise { const payload = await this._session.send('Runtime.callFunction', { functionDeclaration: expression, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b7b626d09f..7dc992813b 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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 }); diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index 4d29be0284..2a384fb169 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -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; - onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise; + onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise; onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; onPageOpen(page: Page): void; @@ -70,7 +69,7 @@ export interface Instrumentation { export interface InstrumentationListener { onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; - onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise; + onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise; onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; onPageOpen?(page: Page): void; diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 40dee4888a..dbbe89d5c2 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -53,7 +53,6 @@ export type SmartHandle = T extends Node ? dom.ElementHandle : JSHandle export interface ExecutionContextDelegate { rawEvaluateJSON(expression: string): Promise; rawEvaluateHandle(expression: string): Promise; - rawCallFunctionNoReply(func: Function, ...args: any[]): void; evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], objectIds: ObjectId[]): Promise; getProperties(context: ExecutionContext, objectId: ObjectId): Promise>; 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, values: any[], objectIds: ObjectId[]): Promise { return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds)); } @@ -151,10 +146,6 @@ export class JSHandle 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(pageFunction: FuncOn, arg?: Arg): Promise { return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg); } diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 70de43b944..d92c5ed226 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -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; 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 }; diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index d552c397e0..115cd3dd94 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -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 { + async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise { // 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; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index c3317fcab4..97476d4b31 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -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 { + async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { 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) { diff --git a/packages/playwright-core/src/server/trace/test/inMemorySnapshotter.ts b/packages/playwright-core/src/server/trace/test/inMemorySnapshotter.ts index f7461599aa..f28c5ef03b 100644 --- a/packages/playwright-core/src/server/trace/test/inMemorySnapshotter.ts +++ b/packages/playwright-core/src/server/trace/test/inMemorySnapshotter.ts @@ -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 { + async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise { 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(); this._snapshotReadyPromises.set(snapshotName, promise); return promise; diff --git a/packages/playwright-core/src/server/webkit/wkExecutionContext.ts b/packages/playwright-core/src/server/webkit/wkExecutionContext.ts index 4416df7a9e..52b8ba3677 100644 --- a/packages/playwright-core/src/server/webkit/wkExecutionContext.ts +++ b/packages/playwright-core/src/server/webkit/wkExecutionContext.ts @@ -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, values: any[], objectIds: string[]): Promise { try { const response = await this._session.send('Runtime.callFunctionOn', { diff --git a/tests/library/snapshotter.spec.ts b/tests/library/snapshotter.spec.ts index b6c45b9252..7ada643dcc 100644 --- a/tests/library/snapshotter.spec.ts +++ b/tests/library/snapshotter.spec.ts @@ -215,20 +215,6 @@ it.describe('snapshots', () => { } }); - it('should capture snapshot target', async ({ page, toImpl, snapshotter }) => { - await page.setContent(''); - { - 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(''); - } - { - 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(''); - } - }); - it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => { await page.setContent(''); { diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 71689fa4c1..e8863b0e1e 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -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 }) => {