From d4d483c56e109db3f959fc7ecf2919131b864ee3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 21 Mar 2026 09:17:35 -0700 Subject: [PATCH 1/2] chore: refactor protocol metainfo for debugger pause and snapshot phases --- .../src/client/channelOwner.ts | 2 +- .../playwright-core/src/server/debugger.ts | 30 +-- .../server/dispatchers/debuggerDispatcher.ts | 6 +- .../src/server/dispatchers/dispatcher.ts | 2 +- .../src/server/trace/recorder/tracing.ts | 28 ++- .../src/utils/isomorphic/protocolFormatter.ts | 7 +- .../src/utils/isomorphic/protocolMetainfo.ts | 194 +++++++-------- packages/protocol/src/protocol.yml | 230 +++++++++++------- packages/trace-viewer/src/ui/actionList.tsx | 3 +- utils/generate_channels.js | 23 +- 10 files changed, 287 insertions(+), 238 deletions(-) diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 3957d0f8bfe7d..80e0247474103 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -16,7 +16,7 @@ import { EventEmitter } from './eventEmitter'; import { ValidationError, maybeFindValidator } from '../protocol/validator'; -import { getMetainfo } from '../utils/isomorphic/protocolFormatter'; +import { getMetainfo } from '../utils/isomorphic/protocolMetainfo'; import { captureLibraryStackTrace } from './clientStackTrace'; import { stringifyStackFrames } from '../utils/isomorphic/stackTrace'; diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts index 2e14c64fa3c05..54d45fd3aa115 100644 --- a/packages/playwright-core/src/server/debugger.ts +++ b/packages/playwright-core/src/server/debugger.ts @@ -17,7 +17,7 @@ import { SdkObject } from './instrumentation'; import { monotonicTime } from '../utils'; import { BrowserContext } from './browserContext'; -import { getMetainfo } from '../utils/isomorphic/protocolFormatter'; +import { getMetainfo } from '../utils/isomorphic/protocolMetainfo'; import type { CallMetadata, InstrumentationListener } from './instrumentation'; @@ -29,7 +29,7 @@ export class Debugger extends SdkObject implements InstrumentationListener { private _pauseAt: PauseAt = {}; private _pausedCallsMetadata = new Map void, sdkObject: SdkObject }>(); private _enabled = false; - private _pauseBeforeInputActions = false; // instead of inside input actions + private _pauseBeforeWaitingActions = false; // instead of inside input actions private _context: BrowserContext; static Events = { @@ -52,24 +52,27 @@ export class Debugger extends SdkObject implements InstrumentationListener { } async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - if (this._muted) + if (this._muted || metadata.internal) return; + const metainfo = getMetainfo(metadata); const pauseOnPauseCall = this._enabled && metadata.type === 'BrowserContext' && metadata.method === 'pause'; - const pauseOnNextStep = !!this._pauseAt.next && shouldPauseBeforeStep(metadata, this._pauseBeforeInputActions); + const pauseBeforeAction = !!this._pauseAt.next && !!metainfo?.pause && (this._pauseBeforeWaitingActions || !metainfo?.isAutoWaiting); const pauseOnLocation = !!this._pauseAt.location && matchesLocation(metadata, this._pauseAt.location); - if (pauseOnPauseCall || pauseOnNextStep || pauseOnLocation) + if (pauseOnPauseCall || pauseBeforeAction || pauseOnLocation) await this._pause(sdkObject, metadata); } async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { - if (this._muted) + if (this._muted || metadata.internal) return; - if (!!this._pauseAt.next && !this._pauseBeforeInputActions) + const metainfo = getMetainfo(metadata); + const pauseBeforeInput = !!this._pauseAt.next && !!metainfo?.pause && !!metainfo?.isAutoWaiting && !this._pauseBeforeWaitingActions; + if (pauseBeforeInput) await this._pause(sdkObject, metadata); } private async _pause(sdkObject: SdkObject, metadata: CallMetadata) { - if (this._muted) + if (this._muted || metadata.internal) return; this._pauseAt = {}; metadata.pauseStartTime = monotonicTime(); @@ -93,8 +96,8 @@ export class Debugger extends SdkObject implements InstrumentationListener { this.emit(Debugger.Events.PausedStateChanged); } - setPauseBeforeInputActions() { - this._pauseBeforeInputActions = true; + setPauseBeforeWaitingActions() { + this._pauseBeforeWaitingActions = true; } setPauseAt(at: { next?: boolean, location?: { file: string, line?: number, column?: number } } = {}) { @@ -121,10 +124,3 @@ function matchesLocation(metadata: CallMetadata, location: { file: string, line? (location.line === undefined || metadata.location.line === location.line) && (location.column === undefined || metadata.location.column === location.column); } - -function shouldPauseBeforeStep(metadata: CallMetadata, includeInputActions: boolean): boolean { - if (metadata.internal) - return false; - const metainfo = getMetainfo(metadata); - return !!metainfo?.pausesBeforeAction || (includeInputActions && !!metainfo?.pausesBeforeInput); -} diff --git a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts index 43f69eb9154c4..b1c94999015e5 100644 --- a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts @@ -53,7 +53,7 @@ export class DebuggerDispatcher extends Dispatcher { if (this._object.isPaused()) throw new Error('Debugger is already paused'); - this._object.setPauseBeforeInputActions(); + this._object.setPauseBeforeWaitingActions(); this._object.setPauseAt({ next: true }); } @@ -66,7 +66,7 @@ export class DebuggerDispatcher extends Dispatcher { if (!this._object.isPaused()) throw new Error('Debugger is not paused'); - this._object.setPauseBeforeInputActions(); + this._object.setPauseBeforeWaitingActions(); this._object.setPauseAt({ next: true }); this._object.resume(); } @@ -74,7 +74,7 @@ export class DebuggerDispatcher extends Dispatcher { if (!this._object.isPaused()) throw new Error('Debugger is not paused'); - this._object.setPauseBeforeInputActions(); + this._object.setPauseBeforeWaitingActions(); this._object.setPauseAt({ location: params.location }); this._object.resume(); } diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index 41b0479a8d6ab..4ba12386dd7ff 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -24,7 +24,7 @@ import { TargetClosedError, isTargetClosedError, serializeError } from '../error import { createRootSdkObject, SdkObject } from '../instrumentation'; import { isProtocolError } from '../protocolError'; import { compressCallLog } from '../callLog'; -import { getMetainfo } from '../../utils/isomorphic/protocolFormatter'; +import { getMetainfo } from '../../utils/isomorphic/protocolMetainfo'; import { Progress, ProgressController } from '../progress'; import type { CallMetadata } from '../instrumentation'; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 54caaf6ad96ed..aef9ebdf3e6f9 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -19,7 +19,7 @@ import os from 'os'; import path from 'path'; import { Snapshotter } from './snapshotter'; -import { getMetainfo } from '../../../utils/isomorphic/protocolFormatter'; +import { getMetainfo } from '../../../utils/isomorphic/protocolMetainfo'; import { assert } from '../../../utils/isomorphic/assert'; import { monotonicTime } from '../../../utils/isomorphic/time'; import { eventsHelper } from '../../utils/eventsHelper'; @@ -435,8 +435,19 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps await this._snapshotter?.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {}); } - private _shouldCaptureSnapshot(sdkObject: SdkObject, metadata: CallMetadata) { - return !!this._snapshotter?.started() && shouldCaptureSnapshot(metadata) && !!sdkObject.attribution.page; + private _shouldCaptureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'input') { + if (!sdkObject.attribution.page || !this._snapshotter?.started()) + return; + + const metainfo = getMetainfo(metadata); + if (!metainfo?.snapshot) + return false; + + switch (phase) { + case 'before': return !metainfo.input || !!metainfo.isAutoWaiting; + case 'input': return !!metainfo.input; + case 'after': return true; + } } onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata, parentId?: string) { @@ -445,7 +456,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - if (this._shouldCaptureSnapshot(sdkObject, metadata)) + if (this._shouldCaptureSnapshot(sdkObject, metadata, 'before')) event.beforeSnapshot = `before@${metadata.id}`; this._state?.callIds.add(metadata.id); this._appendTraceEvent(event); @@ -460,7 +471,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - if (this._shouldCaptureSnapshot(sdkObject, metadata)) + if (this._shouldCaptureSnapshot(sdkObject, metadata, 'input')) event.inputSnapshot = `input@${metadata.id}`; this._appendTraceEvent(event); return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata); @@ -487,7 +498,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - if (this._shouldCaptureSnapshot(sdkObject, metadata)) + if (this._shouldCaptureSnapshot(sdkObject, metadata, 'after')) event.afterSnapshot = `after@${metadata.id}`; this._appendTraceEvent(event); return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata); @@ -659,11 +670,6 @@ function visitTraceEvent(object: any, sha1s: Set): any { return object; } -function shouldCaptureSnapshot(metadata: CallMetadata): boolean { - const metainfo = getMetainfo(metadata); - return !!metainfo?.snapshot; -} - function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string): trace.BeforeActionTraceEvent | null { if (metadata.internal || metadata.method.startsWith('tracing')) return null; diff --git a/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts index eb23d0393e9a2..ab2731b24c83d 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { methodMetainfo } from './protocolMetainfo'; -import type { MethodMetainfo } from './protocolMetainfo'; +import { getMetainfo } from './protocolMetainfo'; export function formatProtocolParam(params: Record | undefined, alternatives: string): string | undefined { return _formatProtocolParam(params, alternatives)?.replaceAll('\n', '\\n'); @@ -70,10 +69,6 @@ export function renderTitleForCall(metadata: { title?: string, type: string, met }); } -export function getMetainfo(metadata: { type: string, method: string }): MethodMetainfo | undefined { - return methodMetainfo.get(metadata.type + '.' + metadata.method); -} - export type ActionGroup = 'configuration' | 'route' | 'getter'; export function getActionGroup(metadata: { type: string, method: string }) { diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 0abc085356e83..0622a444175be 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -16,7 +16,7 @@ // This file is generated by generate_channels.js, do not edit manually. -export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pausesBeforeInput?: boolean, pausesBeforeAction?: boolean, group?: string }; +export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; export const methodMetainfo = new Map([ ['APIRequestContext.fetch', { title: '{method} "{url}"', }], @@ -55,7 +55,7 @@ export const methodMetainfo = new Map([ ['BrowserType.connectOverCDPTransport', { title: 'Connect over CDP transport', }], ['Browser.startServer', { title: 'Start server', }], ['Browser.stopServer', { title: 'Stop server', }], - ['Browser.close', { title: 'Close browser', pausesBeforeAction: true, }], + ['Browser.close', { title: 'Close browser', pause: true, }], ['Browser.killForTests', { internal: true, }], ['Browser.defaultUserAgentForTest', { internal: true, }], ['Browser.newContext', { title: 'Create context', }], @@ -76,7 +76,7 @@ export const methodMetainfo = new Map([ ['BrowserContext.addInitScript', { title: 'Add init script', group: 'configuration', }], ['BrowserContext.clearCookies', { title: 'Clear cookies', group: 'configuration', }], ['BrowserContext.clearPermissions', { title: 'Clear permissions', group: 'configuration', }], - ['BrowserContext.close', { title: 'Close context', pausesBeforeAction: true, }], + ['BrowserContext.close', { title: 'Close context', pause: true, }], ['BrowserContext.cookies', { title: 'Get cookies', group: 'getter', }], ['BrowserContext.exposeBinding', { title: 'Expose binding', group: 'configuration', }], ['BrowserContext.grantPermissions', { title: 'Grant permissions', group: 'configuration', }], @@ -108,35 +108,35 @@ export const methodMetainfo = new Map([ ['BrowserContext.clockSetFixedTime', { title: 'Set fixed time "{timeNumber|timeString}"', }], ['BrowserContext.clockSetSystemTime', { title: 'Set system time "{timeNumber|timeString}"', }], ['Page.addInitScript', { title: 'Add init script', group: 'configuration', }], - ['Page.close', { title: 'Close page', pausesBeforeAction: true, }], + ['Page.close', { title: 'Close page', pause: true, }], ['Page.clearConsoleMessages', { title: 'Clear console messages', }], ['Page.consoleMessages', { title: 'Get console messages', group: 'getter', }], - ['Page.emulateMedia', { title: 'Emulate media', snapshot: true, pausesBeforeAction: true, }], + ['Page.emulateMedia', { title: 'Emulate media', snapshot: true, pause: true, }], ['Page.exposeBinding', { title: 'Expose binding', group: 'configuration', }], - ['Page.goBack', { title: 'Go back', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.goForward', { title: 'Go forward', slowMo: true, snapshot: true, pausesBeforeAction: true, }], + ['Page.goBack', { title: 'Go back', slowMo: true, snapshot: true, pause: true, }], + ['Page.goForward', { title: 'Go forward', slowMo: true, snapshot: true, pause: true, }], ['Page.requestGC', { title: 'Request garbage collection', group: 'configuration', }], ['Page.registerLocatorHandler', { title: 'Register locator handler', }], ['Page.resolveLocatorHandlerNoReply', { internal: true, }], ['Page.unregisterLocatorHandler', { title: 'Unregister locator handler', }], - ['Page.reload', { title: 'Reload', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.expectScreenshot', { title: 'Expect screenshot', snapshot: true, pausesBeforeAction: true, }], - ['Page.screenshot', { title: 'Screenshot', snapshot: true, pausesBeforeAction: true, }], + ['Page.reload', { title: 'Reload', slowMo: true, snapshot: true, pause: true, }], + ['Page.expectScreenshot', { title: 'Expect screenshot', snapshot: true, pause: true, }], + ['Page.screenshot', { title: 'Screenshot', snapshot: true, pause: true, }], ['Page.setExtraHTTPHeaders', { title: 'Set extra HTTP headers', group: 'configuration', }], ['Page.setNetworkInterceptionPatterns', { title: 'Route requests', group: 'route', }], ['Page.setWebSocketInterceptionPatterns', { title: 'Route WebSockets', group: 'route', }], - ['Page.setViewportSize', { title: 'Set viewport size', snapshot: true, pausesBeforeAction: true, }], - ['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.keyboardInsertText', { title: 'Insert "{text}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.keyboardType', { title: 'Type "{text}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.keyboardPress', { title: 'Press "{key}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.mouseMove', { title: 'Mouse move', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.mouseDown', { title: 'Mouse down', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.mouseUp', { title: 'Mouse up', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.mouseClick', { title: 'Click', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.mouseWheel', { title: 'Mouse wheel', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Page.touchscreenTap', { title: 'Tap', slowMo: true, snapshot: true, pausesBeforeAction: true, }], + ['Page.setViewportSize', { title: 'Set viewport size', snapshot: true, pause: true, }], + ['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, pause: true, }], + ['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, pause: true, }], + ['Page.keyboardInsertText', { title: 'Insert "{text}"', slowMo: true, snapshot: true, pause: true, }], + ['Page.keyboardType', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, }], + ['Page.keyboardPress', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, }], + ['Page.mouseMove', { title: 'Mouse move', slowMo: true, snapshot: true, pause: true, }], + ['Page.mouseDown', { title: 'Mouse down', slowMo: true, snapshot: true, pause: true, }], + ['Page.mouseUp', { title: 'Mouse up', slowMo: true, snapshot: true, pause: true, }], + ['Page.mouseClick', { title: 'Click', slowMo: true, snapshot: true, pause: true, }], + ['Page.mouseWheel', { title: 'Mouse wheel', slowMo: true, snapshot: true, pause: true, }], + ['Page.touchscreenTap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, }], ['Page.clearPageErrors', { title: 'Clear page errors', }], ['Page.pageErrors', { title: 'Get page errors', group: 'getter', }], ['Page.pdf', { title: 'PDF', }], @@ -154,104 +154,104 @@ export const methodMetainfo = new Map([ ['Page.videoStop', { title: 'Stop video recording', group: 'configuration', }], ['Page.updateSubscription', { internal: true, }], ['Page.setDockTile', { internal: true, }], - ['Frame.evalOnSelector', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['Frame.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['Frame.addScriptTag', { title: 'Add script tag', snapshot: true, pausesBeforeAction: true, }], - ['Frame.addStyleTag', { title: 'Add style tag', snapshot: true, pausesBeforeAction: true, }], + ['Frame.evalOnSelector', { title: 'Evaluate', snapshot: true, pause: true, }], + ['Frame.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pause: true, }], + ['Frame.addScriptTag', { title: 'Add script tag', snapshot: true, pause: true, }], + ['Frame.addStyleTag', { title: 'Add style tag', snapshot: true, pause: true, }], ['Frame.ariaSnapshot', { title: 'Aria snapshot', group: 'getter', }], - ['Frame.blur', { title: 'Blur', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Frame.check', { title: 'Check', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.click', { title: 'Click', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.content', { title: 'Get content', snapshot: true, pausesBeforeAction: true, }], - ['Frame.dragAndDrop', { title: 'Drag and drop', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.dispatchEvent', { title: 'Dispatch "{type}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Frame.evaluateExpression', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['Frame.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['Frame.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.focus', { title: 'Focus', slowMo: true, snapshot: true, pausesBeforeAction: true, }], + ['Frame.blur', { title: 'Blur', slowMo: true, snapshot: true, pause: true, }], + ['Frame.check', { title: 'Check', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.click', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.content', { title: 'Get content', snapshot: true, pause: true, }], + ['Frame.dragAndDrop', { title: 'Drag and drop', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.dispatchEvent', { title: 'Dispatch "{type}"', slowMo: true, snapshot: true, pause: true, }], + ['Frame.evaluateExpression', { title: 'Evaluate', snapshot: true, pause: true, }], + ['Frame.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pause: true, }], + ['Frame.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.focus', { title: 'Focus', slowMo: true, snapshot: true, pause: true, }], ['Frame.frameElement', { title: 'Get frame element', group: 'getter', }], ['Frame.resolveSelector', { internal: true, }], ['Frame.highlight', { title: 'Highlight element', group: 'configuration', }], - ['Frame.getAttribute', { title: 'Get attribute "{name}"', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.goto', { title: 'Navigate to "{url}"', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['Frame.hover', { title: 'Hover', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.innerHTML', { title: 'Get HTML', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.innerText', { title: 'Get inner text', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.inputValue', { title: 'Get input value', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isChecked', { title: 'Is checked', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isDisabled', { title: 'Is disabled', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isEnabled', { title: 'Is enabled', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isHidden', { title: 'Is hidden', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isVisible', { title: 'Is visible', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.isEditable', { title: 'Is editable', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['Frame.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pausesBeforeInput: true, }], + ['Frame.getAttribute', { title: 'Get attribute "{name}"', snapshot: true, pause: true, group: 'getter', }], + ['Frame.goto', { title: 'Navigate to "{url}"', slowMo: true, snapshot: true, pause: true, }], + ['Frame.hover', { title: 'Hover', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.innerHTML', { title: 'Get HTML', snapshot: true, pause: true, group: 'getter', }], + ['Frame.innerText', { title: 'Get inner text', snapshot: true, pause: true, group: 'getter', }], + ['Frame.inputValue', { title: 'Get input value', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isChecked', { title: 'Is checked', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isDisabled', { title: 'Is disabled', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isEnabled', { title: 'Is enabled', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isHidden', { title: 'Is hidden', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isVisible', { title: 'Is visible', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isEditable', { title: 'Is editable', snapshot: true, pause: true, group: 'getter', }], + ['Frame.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['Frame.querySelector', { title: 'Query selector', snapshot: true, }], ['Frame.querySelectorAll', { title: 'Query selector all', snapshot: true, }], - ['Frame.queryCount', { title: 'Query count', snapshot: true, pausesBeforeAction: true, }], - ['Frame.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.setContent', { title: 'Set content', snapshot: true, pausesBeforeAction: true, }], - ['Frame.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.tap', { title: 'Tap', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.textContent', { title: 'Get text content', snapshot: true, pausesBeforeAction: true, group: 'getter', }], + ['Frame.queryCount', { title: 'Query count', snapshot: true, pause: true, }], + ['Frame.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.setContent', { title: 'Set content', snapshot: true, pause: true, }], + ['Frame.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.tap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.textContent', { title: 'Get text content', snapshot: true, pause: true, group: 'getter', }], ['Frame.title', { title: 'Get page title', group: 'getter', }], - ['Frame.type', { title: 'Type "{text}"', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['Frame.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pausesBeforeInput: true, }], + ['Frame.type', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['Frame.waitForTimeout', { title: 'Wait for timeout', snapshot: true, }], - ['Frame.waitForFunction', { title: 'Wait for function', snapshot: true, pausesBeforeAction: true, }], + ['Frame.waitForFunction', { title: 'Wait for function', snapshot: true, pause: true, }], ['Frame.waitForSelector', { title: 'Wait for selector', snapshot: true, }], - ['Frame.expect', { title: 'Expect "{expression}"', snapshot: true, pausesBeforeAction: true, }], + ['Frame.expect', { title: 'Expect "{expression}"', snapshot: true, pause: true, }], ['Worker.evaluateExpression', { title: 'Evaluate', }], ['Worker.evaluateExpressionHandle', { title: 'Evaluate', }], ['Worker.updateSubscription', { internal: true, }], ['Disposable.dispose', { internal: true, }], ['JSHandle.dispose', { internal: true, }], ['ElementHandle.dispose', { internal: true, }], - ['JSHandle.evaluateExpression', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.evaluateExpression', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['JSHandle.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], + ['JSHandle.evaluateExpression', { title: 'Evaluate', snapshot: true, pause: true, }], + ['ElementHandle.evaluateExpression', { title: 'Evaluate', snapshot: true, pause: true, }], + ['JSHandle.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pause: true, }], + ['ElementHandle.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pause: true, }], ['JSHandle.getPropertyList', { title: 'Get property list', group: 'getter', }], ['ElementHandle.getPropertyList', { title: 'Get property list', group: 'getter', }], ['JSHandle.getProperty', { title: 'Get JS property', group: 'getter', }], ['ElementHandle.getProperty', { title: 'Get JS property', group: 'getter', }], ['JSHandle.jsonValue', { title: 'Get JSON value', group: 'getter', }], ['ElementHandle.jsonValue', { title: 'Get JSON value', group: 'getter', }], - ['ElementHandle.evalOnSelector', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.boundingBox', { title: 'Get bounding box', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.check', { title: 'Check', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.click', { title: 'Click', slowMo: true, snapshot: true, pausesBeforeInput: true, }], + ['ElementHandle.evalOnSelector', { title: 'Evaluate', snapshot: true, pause: true, }], + ['ElementHandle.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pause: true, }], + ['ElementHandle.boundingBox', { title: 'Get bounding box', snapshot: true, pause: true, }], + ['ElementHandle.check', { title: 'Check', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.click', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.contentFrame', { title: 'Get content frame', group: 'getter', }], - ['ElementHandle.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.dispatchEvent', { title: 'Dispatch event', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.focus', { title: 'Focus', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.getAttribute', { title: 'Get attribute', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.hover', { title: 'Hover', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.innerHTML', { title: 'Get HTML', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.innerText', { title: 'Get inner text', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.inputValue', { title: 'Get input value', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isChecked', { title: 'Is checked', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isDisabled', { title: 'Is disabled', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isEditable', { title: 'Is editable', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isEnabled', { title: 'Is enabled', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isHidden', { title: 'Is hidden', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.isVisible', { title: 'Is visible', snapshot: true, pausesBeforeAction: true, group: 'getter', }], + ['ElementHandle.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.dispatchEvent', { title: 'Dispatch event', slowMo: true, snapshot: true, pause: true, }], + ['ElementHandle.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.focus', { title: 'Focus', slowMo: true, snapshot: true, pause: true, }], + ['ElementHandle.getAttribute', { title: 'Get attribute', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.hover', { title: 'Hover', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.innerHTML', { title: 'Get HTML', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.innerText', { title: 'Get inner text', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.inputValue', { title: 'Get input value', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isChecked', { title: 'Is checked', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isDisabled', { title: 'Is disabled', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isEditable', { title: 'Is editable', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isEnabled', { title: 'Is enabled', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isHidden', { title: 'Is hidden', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.isVisible', { title: 'Is visible', snapshot: true, pause: true, group: 'getter', }], ['ElementHandle.ownerFrame', { title: 'Get owner frame', group: 'getter', }], - ['ElementHandle.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pausesBeforeInput: true, }], + ['ElementHandle.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.querySelector', { title: 'Query selector', snapshot: true, }], ['ElementHandle.querySelectorAll', { title: 'Query selector all', snapshot: true, }], - ['ElementHandle.screenshot', { title: 'Screenshot', snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.scrollIntoViewIfNeeded', { title: 'Scroll into view', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.selectText', { title: 'Select text', slowMo: true, snapshot: true, pausesBeforeAction: true, }], - ['ElementHandle.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.tap', { title: 'Tap', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.textContent', { title: 'Get text content', snapshot: true, pausesBeforeAction: true, group: 'getter', }], - ['ElementHandle.type', { title: 'Type', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pausesBeforeInput: true, }], - ['ElementHandle.waitForElementState', { title: 'Wait for state', snapshot: true, pausesBeforeAction: true, }], + ['ElementHandle.screenshot', { title: 'Screenshot', snapshot: true, pause: true, }], + ['ElementHandle.scrollIntoViewIfNeeded', { title: 'Scroll into view', slowMo: true, snapshot: true, pause: true, }], + ['ElementHandle.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.selectText', { title: 'Select text', slowMo: true, snapshot: true, pause: true, }], + ['ElementHandle.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.tap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.textContent', { title: 'Get text content', snapshot: true, pause: true, group: 'getter', }], + ['ElementHandle.type', { title: 'Type', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.waitForElementState', { title: 'Wait for state', snapshot: true, pause: true, }], ['ElementHandle.waitForSelector', { title: 'Wait for selector', snapshot: true, }], ['Request.response', { internal: true, }], ['Request.rawRequestHeaders', { internal: true, }], @@ -333,3 +333,7 @@ export const methodMetainfo = new Map([ ['JsonPipe.send', { internal: true, }], ['JsonPipe.close', { internal: true, }] ]); + +export function getMetainfo(metadata: { type: string, method: string }): MethodMetainfo | undefined { + return methodMetainfo.get(metadata.type + '.' + metadata.method); +} diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index bef14056ef883..2f32f1e7a076e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1060,7 +1060,7 @@ Browser: parameters: reason: string? flags: - pausesBeforeAction: true + pause: true killForTests: internal: true @@ -1246,7 +1246,7 @@ BrowserContext: parameters: reason: string? flags: - pausesBeforeAction: true + pause: true cookies: title: Get cookies @@ -1617,7 +1617,7 @@ Page: runBeforeUnload: boolean? reason: string? flags: - pausesBeforeAction: true + pause: true clearConsoleMessages: title: Clear console messages @@ -1671,7 +1671,7 @@ Page: - no-override flags: snapshot: true - pausesBeforeAction: true + pause: true exposeBinding: title: Expose binding @@ -1692,7 +1692,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true goForward: title: Go forward @@ -1704,7 +1704,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true requestGC: title: Request garbage collection @@ -1739,7 +1739,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true expectScreenshot: title: Expect screenshot @@ -1770,7 +1770,7 @@ Page: items: string flags: snapshot: true - pausesBeforeAction: true + pause: true screenshot: title: Screenshot @@ -1789,7 +1789,7 @@ Page: binary: binary flags: snapshot: true - pausesBeforeAction: true + pause: true setExtraHTTPHeaders: title: Set extra HTTP headers @@ -1837,7 +1837,7 @@ Page: height: int flags: snapshot: true - pausesBeforeAction: true + pause: true keyboardDown: title: Key down "{key}" @@ -1846,7 +1846,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true keyboardUp: title: Key up "{key}" @@ -1855,7 +1855,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true keyboardInsertText: title: Insert "{text}" @@ -1864,7 +1864,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true keyboardType: title: Type "{text}" @@ -1874,7 +1874,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true keyboardPress: title: Press "{key}" @@ -1884,7 +1884,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true mouseMove: title: Mouse move @@ -1895,7 +1895,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true mouseDown: title: Mouse down @@ -1910,7 +1910,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true mouseUp: title: Mouse up @@ -1925,7 +1925,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true mouseClick: title: Click @@ -1943,7 +1943,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true mouseWheel: title: Mouse wheel @@ -1953,7 +1953,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true touchscreenTap: title: Tap @@ -1963,7 +1963,7 @@ Page: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true clearPageErrors: title: Clear page errors @@ -2231,7 +2231,7 @@ Frame: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true evalOnSelectorAll: title: Evaluate @@ -2244,7 +2244,7 @@ Frame: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true addScriptTag: title: Add script tag @@ -2256,7 +2256,7 @@ Frame: element: ElementHandle flags: snapshot: true - pausesBeforeAction: true + pause: true addStyleTag: title: Add style tag @@ -2267,7 +2267,7 @@ Frame: element: ElementHandle flags: snapshot: true - pausesBeforeAction: true + pause: true ariaSnapshot: title: Aria snapshot @@ -2295,7 +2295,7 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true check: title: Check @@ -2309,7 +2309,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true click: title: Click @@ -2343,7 +2345,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true content: title: Get content @@ -2351,7 +2355,7 @@ Frame: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true dragAndDrop: title: Drag and drop @@ -2368,7 +2372,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true dblclick: title: Double click @@ -2400,7 +2406,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true dispatchEvent: title: Dispatch "{type}" @@ -2413,7 +2421,7 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true evaluateExpression: title: Evaluate @@ -2425,7 +2433,7 @@ Frame: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true evaluateExpressionHandle: title: Evaluate @@ -2437,7 +2445,7 @@ Frame: handle: JSHandle flags: snapshot: true - pausesBeforeAction: true + pause: true fill: title: Fill "{value}" @@ -2450,7 +2458,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true focus: title: Focus @@ -2461,7 +2471,7 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true frameElement: title: Get frame element @@ -2494,7 +2504,7 @@ Frame: value: string? flags: snapshot: true - pausesBeforeAction: true + pause: true goto: title: Navigate to "{url}" @@ -2508,7 +2518,7 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true hover: title: Hover @@ -2532,7 +2542,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true innerHTML: title: Get HTML @@ -2545,7 +2557,7 @@ Frame: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true innerText: title: Get inner text @@ -2558,7 +2570,7 @@ Frame: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true inputValue: title: Get input value @@ -2571,7 +2583,7 @@ Frame: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true isChecked: title: Is checked @@ -2584,7 +2596,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isDisabled: title: Is disabled @@ -2597,7 +2609,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isEnabled: title: Is enabled @@ -2610,7 +2622,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isHidden: title: Is hidden @@ -2623,7 +2635,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isVisible: title: Is visible @@ -2636,7 +2648,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isEditable: title: Is editable @@ -2649,7 +2661,7 @@ Frame: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true press: title: Press "{key}" @@ -2663,7 +2675,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true querySelector: title: Query selector @@ -2694,7 +2708,7 @@ Frame: value: int flags: snapshot: true - pausesBeforeAction: true + pause: true selectOption: title: Select option @@ -2722,7 +2736,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true setContent: title: Set content @@ -2732,7 +2748,7 @@ Frame: waitUntil: LifecycleEvent? flags: snapshot: true - pausesBeforeAction: true + pause: true setInputFiles: title: Set input files @@ -2760,7 +2776,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true tap: title: Tap @@ -2784,7 +2802,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true textContent: title: Get text content @@ -2797,7 +2817,7 @@ Frame: value: string? flags: snapshot: true - pausesBeforeAction: true + pause: true title: title: Get page title @@ -2816,7 +2836,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true uncheck: title: Uncheck @@ -2830,7 +2852,9 @@ Frame: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true waitForTimeout: title: Wait for timeout @@ -2852,7 +2876,7 @@ Frame: handle: JSHandle flags: snapshot: true - pausesBeforeAction: true + pause: true waitForSelector: title: Wait for selector @@ -2897,7 +2921,7 @@ Frame: items: string flags: snapshot: true - pausesBeforeAction: true + pause: true events: @@ -2990,7 +3014,7 @@ JSHandle: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true evaluateExpressionHandle: title: Evaluate @@ -3002,7 +3026,7 @@ JSHandle: handle: JSHandle flags: snapshot: true - pausesBeforeAction: true + pause: true getPropertyList: title: Get property list @@ -3057,7 +3081,7 @@ ElementHandle: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true evalOnSelectorAll: title: Evaluate @@ -3070,7 +3094,7 @@ ElementHandle: value: SerializedValue flags: snapshot: true - pausesBeforeAction: true + pause: true boundingBox: title: Get bounding box @@ -3078,7 +3102,7 @@ ElementHandle: value: Rect? flags: snapshot: true - pausesBeforeAction: true + pause: true check: title: Check @@ -3090,7 +3114,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true click: title: Click @@ -3122,7 +3148,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true contentFrame: title: Get content frame @@ -3158,7 +3186,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true dispatchEvent: title: Dispatch event @@ -3168,7 +3198,7 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true fill: title: Fill "{value}" @@ -3179,14 +3209,16 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true focus: title: Focus flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true getAttribute: title: Get attribute @@ -3197,7 +3229,7 @@ ElementHandle: value: string? flags: snapshot: true - pausesBeforeAction: true + pause: true hover: title: Hover @@ -3219,7 +3251,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true innerHTML: title: Get HTML @@ -3228,7 +3262,7 @@ ElementHandle: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true innerText: title: Get inner text @@ -3237,7 +3271,7 @@ ElementHandle: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true inputValue: title: Get input value @@ -3246,7 +3280,7 @@ ElementHandle: value: string flags: snapshot: true - pausesBeforeAction: true + pause: true isChecked: title: Is checked @@ -3255,7 +3289,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isDisabled: title: Is disabled @@ -3264,7 +3298,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isEditable: title: Is editable @@ -3273,7 +3307,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isEnabled: title: Is enabled @@ -3282,7 +3316,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isHidden: title: Is hidden @@ -3291,7 +3325,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true isVisible: title: Is visible @@ -3300,7 +3334,7 @@ ElementHandle: value: boolean flags: snapshot: true - pausesBeforeAction: true + pause: true ownerFrame: title: Get owner frame @@ -3318,7 +3352,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true querySelector: title: Query selector @@ -3356,7 +3392,7 @@ ElementHandle: binary: binary flags: snapshot: true - pausesBeforeAction: true + pause: true scrollIntoViewIfNeeded: title: Scroll into view @@ -3365,7 +3401,7 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true selectOption: title: Select option @@ -3391,7 +3427,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true selectText: title: Select text @@ -3401,7 +3439,7 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeAction: true + pause: true setInputFiles: title: Set input files @@ -3427,7 +3465,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true tap: title: Tap @@ -3449,7 +3489,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true textContent: title: Get text content @@ -3458,7 +3500,7 @@ ElementHandle: value: string? flags: snapshot: true - pausesBeforeAction: true + pause: true type: title: Type @@ -3469,7 +3511,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true uncheck: title: Uncheck @@ -3481,7 +3525,9 @@ ElementHandle: flags: slowMo: true snapshot: true - pausesBeforeInput: true + pause: true + input: true + isAutoWaiting: true waitForElementState: title: Wait for state @@ -3498,7 +3544,7 @@ ElementHandle: timeout: float flags: snapshot: true - pausesBeforeAction: true + pause: true waitForSelector: title: Wait for selector diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index b019bba28e5f8..8f4e6ccaa321b 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -27,7 +27,8 @@ import type { ActionTraceEventInContext, ActionTreeItem } from '@isomorphic/trac import type { Boundaries } from './geometry'; import { ToolbarButton } from '@web/components/toolbarButton'; import { testStatusIcon } from './testUtils'; -import { formatProtocolParam, getMetainfo } from '@isomorphic/protocolFormatter'; +import { getMetainfo } from '@isomorphic/protocolMetainfo'; +import { formatProtocolParam } from '@isomorphic/protocolFormatter'; export interface ActionListProps { actions: ActionTraceEventInContext[], diff --git a/utils/generate_channels.js b/utils/generate_channels.js index aa0fb884f7b86..3bc621d5d42cb 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -288,12 +288,8 @@ for (const [name, item] of Object.entries(protocol)) { throw new Error(`Method "${className}.${methodName}" has "slowMo" flag, so cannot be "internal" in protocol.yml`); if (method.flags?.snapshot && method.internal) throw new Error(`Method "${className}.${methodName}" has "snapshot" flag, so cannot be "internal" in protocol.yml`); - if (method.flags?.pausesBeforeInput && method.internal) - throw new Error(`Method "${className}.${methodName}" has "pausesBeforeInput" flag, so cannot be "internal" in protocol.yml`); - if (method.flags?.pausesBeforeAction && method.internal) - throw new Error(`Method "${className}.${methodName}" has "pausesBeforeAction" flag, so cannot be "internal" in protocol.yml`); - if (method.flags?.pausesBeforeInput && method.flags?.pausesBeforeAction) - throw new Error(`Method "${className}.${methodName}" cannot have both "pausesBeforeInput" and "pausesBeforeAction" flags in protocol.yml`); + if (method.flags?.pause && method.internal) + throw new Error(`Method "${className}.${methodName}" has "pause" flag, so cannot be "internal" in protocol.yml`); if (!method.title && !method.internal) throw new Error(`Method "${className}.${methodName}" must have a "title" because it is not "internal" in protocol.yml`); if (method.group && method.internal) @@ -305,9 +301,10 @@ for (const [name, item] of Object.entries(protocol)) { const groupProp = method.group ? ` group: '${method.group}',` : ''; const slowMoProp = method.flags?.slowMo ? ` slowMo: ${method.flags.slowMo},` : ''; const snapshotProp = method.flags?.snapshot ? ` snapshot: ${method.flags.snapshot},` : ''; - const pausesBeforeInputProp = method.flags?.pausesBeforeInput ? ` pausesBeforeInput: ${method.flags.pausesBeforeInput},` : ''; - const pausesBeforeActionProp = method.flags?.pausesBeforeAction ? ` pausesBeforeAction: ${method.flags.pausesBeforeAction},` : ''; - methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${slowMoProp}${snapshotProp}${pausesBeforeInputProp}${pausesBeforeActionProp}${groupProp} }]`); + const pauseProp = method.flags?.pause ? ` pause: ${method.flags.pause},` : ''; + const inputProp = method.flags?.input ? ` input: ${method.flags.input},` : ''; + const isAutoWaitingProp = method.flags?.isAutoWaiting ? ` isAutoWaiting: ${method.flags.isAutoWaiting},` : ''; + methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`); } const parameters = objectType(method.parameters || {}, ''); @@ -351,11 +348,15 @@ for (const [name, item] of Object.entries(protocol)) { } } -metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pausesBeforeInput?: boolean, pausesBeforeAction?: boolean, group?: string }; +metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; export const methodMetainfo = new Map([ ${methodMetainfo.join(`,\n `)} -]);`); +]); + +export function getMetainfo(metadata: { type: string, method: string }): MethodMetainfo | undefined { + return methodMetainfo.get(metadata.type + '.' + metadata.method); +}`); let hasChanges = false; From c9c52b2037b68c268a03d2ad606ea3c79aaf15c0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 21 Mar 2026 13:20:26 -0700 Subject: [PATCH 2/2] feat(annotate): instrument input actions and show subtitle overlay --- CLAUDE.md | 1 + docs/src/test-api/class-testoptions.md | 4 + docs/src/videos.md | 8 +- examples/todomvc/playwright.config.ts | 1 + packages/injected/src/highlight.css | 17 ++++ packages/injected/src/highlight.ts | 38 +++++++-- packages/injected/src/injectedScript.ts | 15 ++-- .../src/server/dispatchers/pageDispatcher.ts | 26 +++--- packages/playwright-core/src/server/dom.ts | 73 +++++++++------- packages/playwright-core/src/server/input.ts | 63 +++++++++++++- .../playwright-core/src/server/recorder.ts | 3 - .../playwright-core/src/server/screencast.ts | 35 +++++++- .../src/utils/isomorphic/protocolMetainfo.ts | 22 ++--- .../isomorphic/trace/snapshotRenderer.ts | 84 ++++++++----------- packages/playwright/src/index.ts | 1 + packages/playwright/types/test.d.ts | 5 +- packages/protocol/src/callMetadata.d.ts | 5 +- packages/protocol/src/protocol.yml | 11 +++ packages/trace-viewer/src/ui/snapshotTab.tsx | 8 +- tests/library/trace-viewer.spec.ts | 4 +- utils/generate_types/overrides-test.d.ts | 2 +- 21 files changed, 285 insertions(+), 141 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 15e5d82a420e5..a18300875866b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,6 +126,7 @@ EOF Never add Co-Authored-By agents in commit message. Never add "Generated with" in commit message. +Never add test plan to PR description. Keep PR description short — a few bullet points at most. Branch naming for issue fixes: `fix-` ## Development Guides diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 722edbc09ce00..8b43a9849a431 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -625,6 +625,8 @@ export default defineConfig({ - `size` ?<[Object]> Size of the recorded video. Optional. - `width` <[int]> - `height` <[int]> + - `annotate` ?<[Object]> If specified, visually annotates actions in the video with element highlights and action title subtitles. + - `delay` ?<[int]> How long each annotation is displayed in milliseconds. Defaults to `500`. Whether to record video for each test. Defaults to `'off'`. * `'off'`: Do not record video. @@ -634,6 +636,8 @@ Whether to record video for each test. Defaults to `'off'`. To control video size, pass an object with `mode` and `size` properties. If video size is not specified, it will be equal to [`property: TestOptions.viewport`] scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will be scaled down if necessary to fit the specified size. +To annotate actions in the video with element highlights and action title subtitles, pass `annotate` with an optional `delay` in milliseconds (defaults to `500`). + **Usage** ```js title="playwright.config.ts" diff --git a/docs/src/videos.md b/docs/src/videos.md index 60f740cf8b99d..6fabf0c123d39 100644 --- a/docs/src/videos.md +++ b/docs/src/videos.md @@ -36,7 +36,9 @@ const context = await browser.newContext({ recordVideo: { dir: 'videos/' } }); await context.close(); ``` -You can also specify video size. The video size defaults to the viewport size scaled down to fit 800x800. The video of the viewport is placed in the top-left corner of the output video, scaled down to fit if necessary. You may need to set the viewport size to match your desired video size. +You can also specify video size and annotation. The video size defaults to the viewport size scaled down to fit 800x800. The video of the viewport is placed in the top-left corner of the output video, scaled down to fit if necessary. You may need to set the viewport size to match your desired video size. + +When `annotate` is specified, each action will be visually highlighted in the video with the element outline and action title subtitle. The optional `delay` property controls how long each annotation is displayed (defaults to `500`ms). ```js tab=js-test title="playwright.config.ts" import { defineConfig } from '@playwright/test'; @@ -44,7 +46,8 @@ export default defineConfig({ use: { video: { mode: 'on-first-retry', - size: { width: 640, height: 480 } + size: { width: 640, height: 480 }, + annotate: { delay: 500 }, } }, }); @@ -55,6 +58,7 @@ const context = await browser.newContext({ recordVideo: { dir: 'videos/', size: { width: 640, height: 480 }, + annotate: { delay: 500 }, } }); ``` diff --git a/examples/todomvc/playwright.config.ts b/examples/todomvc/playwright.config.ts index 2cb249b0377c9..5004cd18e24dd 100644 --- a/examples/todomvc/playwright.config.ts +++ b/examples/todomvc/playwright.config.ts @@ -47,6 +47,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + video: { mode: 'on', annotate: { delay: 500 } }, }, /* Configure projects for major browsers */ diff --git a/packages/injected/src/highlight.css b/packages/injected/src/highlight.css index f4095467f88c4..9c10148a66668 100644 --- a/packages/injected/src/highlight.css +++ b/packages/injected/src/highlight.css @@ -107,6 +107,23 @@ x-pw-action-point { z-index: 2; } +x-pw-subtitle { + position: absolute; + bottom: 55px; + left: 50%; + transform: translateX(-50%); + backdrop-filter: blur(5px); + background-color: rgba(0, 0, 0, 0.7); + color: white; + border-radius: 6px; + padding: 6px 14px; + font-size: 14px; + line-height: 1.4; + white-space: nowrap; + user-select: none; + z-index: 3; +} + @keyframes pw-fade-out { from { opacity: 1; } to { opacity: 0; } diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index 3319d26056043..8c221aac852ff 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -24,8 +24,10 @@ import type { ParsedSelector } from '@isomorphic/selectorParser'; import type { InjectedScript } from './injectedScript'; +type Rect = { x: number, y: number, width: number, height: number }; + type RenderedHighlightEntry = { - targetElement: Element, + targetElement?: Element, color: string, borderColor?: string, fadeDuration?: number, @@ -38,7 +40,8 @@ type RenderedHighlightEntry = { }; export type HighlightEntry = { - element: Element, + element?: Element, + box?: Rect, color: string, borderColor?: string, fadeDuration?: number, @@ -50,6 +53,7 @@ export class Highlight { private _glassPaneShadow: ShadowRoot; private _renderedEntries: RenderedHighlightEntry[] = []; private _actionPointElement: HTMLElement; + private _subtitleElement: HTMLElement; private _isUnderTest: boolean; private _injectedScript: InjectedScript; private _rafRequest: number | undefined; @@ -75,6 +79,8 @@ export class Highlight { this._glassPaneElement.style.backgroundColor = 'transparent'; this._actionPointElement = document.createElement('x-pw-action-point'); this._actionPointElement.setAttribute('hidden', 'true'); + this._subtitleElement = document.createElement('x-pw-subtitle'); + this._subtitleElement.setAttribute('hidden', 'true'); this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' }); // workaround for firefox: when taking screenshots, it complains adoptedStyleSheets.push // is not a function, so we fallback to style injection @@ -88,6 +94,7 @@ export class Highlight { this._glassPaneShadow.appendChild(styleElement); } this._glassPaneShadow.appendChild(this._actionPointElement); + this._glassPaneShadow.appendChild(this._subtitleElement); } install() { @@ -141,6 +148,17 @@ export class Highlight { this._actionPointElement.hidden = true; } + showSubtitle(text: string, fadeDuration: number) { + this._subtitleElement.textContent = text; + this._subtitleElement.hidden = false; + const fadeTime = fadeDuration / 4; + this._subtitleElement.style.animation = `pw-fade-out ${fadeTime}ms ease-out ${fadeDuration - fadeTime}ms forwards`; + } + + hideSubtitle() { + this._subtitleElement.hidden = true; + } + clearHighlight() { for (const entry of this._renderedEntries) { entry.highlightElement?.remove(); @@ -178,12 +196,14 @@ export class Highlight { lineElement.textContent = entry.tooltipText; tooltipElement.appendChild(lineElement); } - this._renderedEntries.push({ targetElement: entry.element, color: entry.color, borderColor: entry.borderColor, fadeDuration: entry.fadeDuration, tooltipElement, highlightElement }); + this._renderedEntries.push({ targetElement: entry.element, box: toDOMRect(entry.box), color: entry.color, borderColor: entry.borderColor, fadeDuration: entry.fadeDuration, tooltipElement, highlightElement }); } // 2. Trigger layout while positioning tooltips and computing bounding boxes. for (const entry of this._renderedEntries) { - entry.box = entry.targetElement.getBoundingClientRect(); + if (!entry.box && !entry.targetElement) + continue; + entry.box = entry.box || entry.targetElement!.getBoundingClientRect(); if (!entry.tooltipElement) continue; @@ -271,7 +291,7 @@ export class Highlight { const oldBox = this._renderedEntries[i].box; if (!oldBox) return false; - const box = entries[i].element.getBoundingClientRect(); + const box = entries[i].box ? toDOMRect(entries[i].box!) : entries[i].element!.getBoundingClientRect(); if (box.top !== oldBox.top || box.right !== oldBox.right || box.bottom !== oldBox.bottom || box.left !== oldBox.left) return false; } @@ -298,3 +318,11 @@ export class Highlight { this._glassPaneElement.removeEventListener('click', handler); } } + +function toDOMRect(box: Rect): DOMRect; +function toDOMRect(box: Rect | undefined): DOMRect | undefined; +function toDOMRect(box: Rect | undefined): DOMRect | undefined { + if (!box) + return undefined; + return new DOMRect(box.x, box.y, box.width, box.height); +} diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index d9ef2532def3d..46ca82ea6b915 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1313,21 +1313,22 @@ export class InjectedScript { this._highlight.runHighlightOnRaf(selector); } - highlightNode(node: Node, point?: { x: number, y: number }, delay?: number) { + annotate(annotation: { point?: channels.Point, box?: channels.Rect, title?: string, delay?: number }) { const highlight = this._createHighlight(); - const fadeDuration = delay ?? 500; + const fadeDuration = annotation.delay ?? 500; - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element; + if (annotation.box) { highlight.updateHighlight([{ - element, + box: annotation.box, color: 'rgba(0, 128, 255, 0.15)', borderColor: 'rgba(0, 128, 255, 0.6)', fadeDuration, }]); } - if (point) - highlight.showActionPoint(point.x, point.y, fadeDuration); + if (annotation.point) + highlight.showActionPoint(annotation.point.x, annotation.point.y, fadeDuration); + if (annotation.title) + highlight.showSubtitle(annotation.title, fadeDuration); } hideHighlight() { diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 97919551fec7d..8657f1b4d67e6 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -261,23 +261,23 @@ export class PageDispatcher extends Dispatcher { - await this._page.keyboard.down(progress, params.key); + await this._page.keyboard.apiDown(progress, params.key); } async keyboardUp(params: channels.PageKeyboardUpParams, progress: Progress): Promise { - await this._page.keyboard.up(progress, params.key); + await this._page.keyboard.apiUp(progress, params.key); } async keyboardInsertText(params: channels.PageKeyboardInsertTextParams, progress: Progress): Promise { - await this._page.keyboard.insertText(progress, params.text); + await this._page.keyboard.apiInsertText(progress, params.text); } async keyboardType(params: channels.PageKeyboardTypeParams, progress: Progress): Promise { - await this._page.keyboard.type(progress, params.text, params); + await this._page.keyboard.apiType(progress, params.text, params); } async keyboardPress(params: channels.PageKeyboardPressParams, progress: Progress): Promise { - await this._page.keyboard.press(progress, params.key, params); + await this._page.keyboard.apiPress(progress, params.key, params); } async clearConsoleMessages(params: channels.PageClearConsoleMessagesParams, progress: Progress): Promise { @@ -301,32 +301,28 @@ export class PageDispatcher extends Dispatcher { - progress.metadata.point = { x: params.x, y: params.y }; - await this._page.mouse.move(progress, params.x, params.y, params); + await this._page.mouse.apiMove(progress, params.x, params.y, params); } async mouseDown(params: channels.PageMouseDownParams, progress: Progress): Promise { - progress.metadata.point = this._page.mouse.currentPoint(); - await this._page.mouse.down(progress, params); + await this._page.mouse.apiDown(progress, params); } async mouseUp(params: channels.PageMouseUpParams, progress: Progress): Promise { - progress.metadata.point = this._page.mouse.currentPoint(); - await this._page.mouse.up(progress, params); + await this._page.mouse.apiUp(progress, params); } async mouseClick(params: channels.PageMouseClickParams, progress: Progress): Promise { - progress.metadata.point = { x: params.x, y: params.y }; - await this._page.mouse.click(progress, params.x, params.y, params); + await this._page.mouse.apiClick(progress, params.x, params.y, params); } async mouseWheel(params: channels.PageMouseWheelParams, progress: Progress): Promise { - await this._page.mouse.wheel(progress, params.deltaX, params.deltaY); + await this._page.mouse.apiWheel(progress, params.deltaX, params.deltaY); } async touchscreenTap(params: channels.PageTouchscreenTapParams, progress: Progress): Promise { progress.metadata.point = { x: params.x, y: params.y }; - await this._page.touchscreen.tap(progress, params.x, params.y); + await this._page.touchscreen.apiTap(progress, params.x, params.y); } async pdf(params: channels.PagePdfParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index eddf9af75914f..773569fa8724d 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -227,7 +227,7 @@ export class ElementHandle extends js.JSHandle { await this._waitAndScrollIntoViewIfNeeded(progress, false /* waitForVisible */); } - private async _clickablePoint(): Promise { + private async _clickablePoint(): Promise<{ point: types.Point, box: types.Rect } | 'error:notvisible' | 'error:notinviewport' | 'error:notconnected'> { const intersectQuadWithViewport = (quad: types.Quad): types.Quad => { return quad.map(point => ({ x: Math.min(Math.max(point.x, 0), metrics.width), @@ -260,6 +260,8 @@ export class ElementHandle extends js.JSHandle { const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 0.99); if (!filtered.length) return 'error:notinviewport'; + const quad = filtered[0]; + const box = quadToRect(quad); if (this._page.browserContext._browser.options.name === 'firefox') { // Firefox internally uses integer coordinates, so 8.x is converted to 8 or 9 when clicking. // @@ -268,17 +270,17 @@ export class ElementHandle extends js.JSHandle { // So, clicking at (8.x;8.y) will sometimes click at (9;9) and miss the target. // // Therefore, we try to find an integer point within a quad to make sure we click inside the element. - for (const quad of filtered) { - const integerPoint = findIntegerPointInsideQuad(quad); + for (const q of filtered) { + const integerPoint = findIntegerPointInsideQuad(q); if (integerPoint) - return integerPoint; + return { point: integerPoint, box }; } } // Return the middle point of the first quad. - return quadMiddlePoint(filtered[0]); + return { point: quadMiddlePoint(quad), box }; } - private async _offsetPoint(offset: types.Point): Promise { + private async _offsetPoint(offset: types.Point): Promise<{ point: types.Point, box: types.Rect } | 'error:notvisible' | 'error:notconnected'> { const [box, border] = await Promise.all([ this.boundingBox(), this.evaluateInUtility(([injected, node]) => injected.getElementBorderWidth(node), {}).catch(e => {}), @@ -289,8 +291,11 @@ export class ElementHandle extends js.JSHandle { return border; // Make point relative to the padding box to align with offsetX/offsetY. return { - x: box.x + border.left + offset.x, - y: box.y + border.top + offset.y, + point: { + x: box.x + border.left + offset.x, + y: box.y + border.top + offset.y, + }, + box, }; } @@ -377,18 +382,6 @@ export class ElementHandle extends js.JSHandle { }, { ...options, skipActionPreChecks }); } - private async _beforeAction(progress: Progress, point?: types.Point): Promise { - await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); - const annotate = progress.metadata.annotate; - if (annotate) { - await progress.race(this.evaluateInUtility(async ([injected, node, options]) => { - injected.highlightNode(node, options.point, options.delay); - await new Promise(f => injected.utils.builtins.setTimeout(f, options.delay)); - injected.hideHighlight(); - }, { point, delay: annotate.delay })); - } - } - async _performPointerAction( progress: Progress, actionName: ActionName, @@ -440,12 +433,13 @@ export class ElementHandle extends js.JSHandle { return scrolled; progress.log(' done scrolling'); - const maybePoint = position ? await progress.race(this._offsetPoint(position)) : await progress.race(this._clickablePoint()); - if (typeof maybePoint === 'string') - return maybePoint; - const point = roundPoint(maybePoint); + const maybeResult = position ? await progress.race(this._offsetPoint(position)) : await progress.race(this._clickablePoint()); + if (typeof maybeResult === 'string') + return maybeResult; + const point = roundPoint(maybeResult.point); progress.metadata.point = point; - await this._beforeAction(progress, point); + progress.metadata.box = maybeResult.box; + await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); let hitTargetInterceptionHandle: js.JSHandle | undefined; if (force) { @@ -567,10 +561,16 @@ export class ElementHandle extends js.JSHandle { return throwRetargetableDOMError(result); } + private async _beforeNonPointerAction(progress: Progress) { + if (progress.metadata.annotate) + progress.metadata.box = await this.boundingBox() || undefined; + await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + } + async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { let resultingOptions: string[] = []; const result = await this._retryAction(progress, 'select option', async () => { - await this._beforeAction(progress); + await this._beforeNonPointerAction(progress); if (!options.force) progress.log(` waiting for element to be visible and enabled`); const optionsToSelect = [...elements, ...values]; @@ -603,7 +603,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 this._beforeAction(progress); + await this._beforeNonPointerAction(progress); if (!options.force) progress.log(' waiting for element to be visible, enabled and editable'); const result = await progress.race(this.evaluateInUtility(async ([injected, node, { value, force }]) => { @@ -616,7 +616,7 @@ export class ElementHandle extends js.JSHandle { }, { value, force: options.force })); if (result === 'needsinput') { if (value) - await this._page.keyboard.insertText(progress, value); + await this._page.keyboard._insertText(progress, value); else await this._page.keyboard.press(progress, 'Delete'); return 'done'; @@ -670,7 +670,7 @@ export class ElementHandle extends js.JSHandle { if (result === 'error:notconnected' || !result.asElement()) return 'error:notconnected'; const retargeted = result.asElement() as ElementHandle; - await this._beforeAction(progress); + await this._beforeNonPointerAction(progress); if (localPaths || localDirectory) { const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!; await progress.race(Promise.all((localPathsOrDirectory).map(localPath => ( @@ -711,7 +711,7 @@ export class ElementHandle extends js.JSHandle { async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.type("${text}")`); - await this._beforeAction(progress); + await this._beforeNonPointerAction(progress); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') return result; @@ -727,7 +727,7 @@ export class ElementHandle extends js.JSHandle { async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.press("${key}")`); - await this._beforeAction(progress); + await this._beforeNonPointerAction(progress); return this._page.frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => { const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') @@ -900,6 +900,17 @@ function roundPoint(point: types.Point): types.Point { }; } +function quadToRect(quad: types.Quad): types.Rect { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const point of quad) { + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + } + return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; +} + function quadMiddlePoint(quad: types.Quad): types.Point { const result = { x: 0, y: 0 }; for (const point of quad) { diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index b1e85186d6b3d..a7eec2f4bfb10 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -53,6 +53,11 @@ export class Keyboard { this._page = page; } + async apiDown(progress: Progress, key: string) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.down(progress, key); + } + async down(progress: Progress, key: string) { const description = this._keyDescriptionForString(key); const autoRepeat = this._pressedKeys.has(description.code); @@ -76,6 +81,11 @@ export class Keyboard { return description; } + async apiUp(progress: Progress, key: string) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.up(progress, key); + } + async up(progress: Progress, key: string) { const description = this._keyDescriptionForString(key); if (kModifiers.includes(description.key as types.KeyboardModifier)) @@ -84,10 +94,20 @@ export class Keyboard { await this._raw.keyup(progress, this._pressedModifiers, key, description); } - async insertText(progress: Progress, text: string) { + async apiInsertText(progress: Progress, text: string) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this._insertText(progress, text); + } + + async _insertText(progress: Progress, text: string) { await this._raw.sendText(progress, text); } + async apiType(progress: Progress, text: string, options?: { delay?: number }) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.type(progress, text, options); + } + async type(progress: Progress, text: string, options?: { delay?: number }) { const delay = (options && options.delay) || undefined; for (const char of text) { @@ -96,11 +116,16 @@ export class Keyboard { } else { if (delay) await progress.wait(delay); - await this.insertText(progress, char); + await this._insertText(progress, char); } } } + async apiPress(progress: Progress, key: string, options: { delay?: number } = {}) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.press(progress, key, options); + } + async press(progress: Progress, key: string, options: { delay?: number } = {}) { function split(keyString: string) { const keys = []; @@ -188,6 +213,12 @@ export class Mouse { return { x: this._x, y: this._y }; } + async apiMove(progress: Progress, x: number, y: number, options: { steps?: number, forClick?: boolean } = {}) { + progress.metadata.point = { x, y }; + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.move(progress, x, y, options); + } + async move(progress: Progress, x: number, y: number, options: { steps?: number, forClick?: boolean } = {}) { const { steps = 1 } = options; const fromX = this._x; @@ -201,6 +232,12 @@ export class Mouse { } } + async apiDown(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { + progress.metadata.point = this.currentPoint(); + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.down(progress, options); + } + async down(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { const { button = 'left', clickCount = 1 } = options; this._lastButton = button; @@ -208,6 +245,12 @@ export class Mouse { await this._raw.down(progress, this._x, this._y, this._lastButton, this._buttons, this._keyboard._modifiers(), clickCount); } + async apiUp(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { + progress.metadata.point = this.currentPoint(); + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.up(progress, options); + } + async up(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { const { button = 'left', clickCount = 1 } = options; this._lastButton = 'none'; @@ -215,6 +258,12 @@ export class Mouse { await this._raw.up(progress, this._x, this._y, button, this._buttons, this._keyboard._modifiers(), clickCount); } + async apiClick(progress: Progress, x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number, steps?: number } = {}) { + progress.metadata.point = { x, y }; + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.click(progress, x, y, options); + } + async click(progress: Progress, x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number, steps?: number } = {}) { const { delay = null, clickCount = 1, steps } = options; if (delay) { @@ -241,7 +290,8 @@ export class Mouse { } } - async wheel(progress: Progress, deltaX: number, deltaY: number) { + async apiWheel(progress: Progress, deltaX: number, deltaY: number) { + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); await this._raw.wheel(progress, this._x, this._y, this._buttons, this._keyboard._modifiers(), deltaX, deltaY); } } @@ -319,9 +369,14 @@ export class Touchscreen { this._page = page; } - async tap(progress: Progress, x: number, y: number) { + async apiTap(progress: Progress, x: number, y: number) { if (!this._page.browserContext._options.hasTouch) throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.'); + await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await this.tap(progress, x, y); + } + + async tap(progress: Progress, x: number, y: number) { await this._raw.tap(progress, x, y, this._page.keyboard._modifiers()); } } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 08fcb4a6d60e3..7eff6de95fc49 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -451,9 +451,6 @@ export class Recorder extends EventEmitter implements Instrume this.emit(RecorderEvent.UserSourcesChanged, this.userSources(), this.pausedSourceId()); } - async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { - } - async onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): Promise { this.updateCallLog([metadata]); } diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index 14ef2a6073479..d518eba615f15 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import { assert, createGuid } from '../utils'; +import { assert, createGuid, renderTitleForCall } from '../utils'; import { debugLogger } from '../utils'; import { VideoRecorder } from './videoRecorder'; import { Page } from './page'; @@ -171,9 +171,36 @@ export class Screencast implements InstrumentationListener { listener(frame); } - async onBeforeInputAction(_: SdkObject, metadata: CallMetadata): Promise { - if (this._annotate) - metadata.annotate = { delay: this._annotate.delay ?? 500 }; + async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata, parentId?: string): Promise { + if (!this._annotate) + return; + metadata.annotate = true; + } + + async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { + if (!this._annotate) + return; + + const page = sdkObject.attribution.page; + if (!page) + return; + + const title = renderTitleForCall(metadata); + const utility = await page.mainFrame()._utilityContext(); + + // Run this outside of the progress timer. + await utility.evaluate(async options => { + const { injected, delay } = options; + injected.annotate(options); + await new Promise(f => injected.utils.builtins.setTimeout(f, delay)); + injected.hideHighlight(); + }, { + injected: await utility.injectedScript(), + ...this._annotate, + point: metadata.point, + box: metadata.box, + title, + }).catch(e => debugLogger.log('error', e)); } } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 0622a444175be..819dfd0aa5dbf 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -126,17 +126,17 @@ export const methodMetainfo = new Map([ ['Page.setNetworkInterceptionPatterns', { title: 'Route requests', group: 'route', }], ['Page.setWebSocketInterceptionPatterns', { title: 'Route WebSockets', group: 'route', }], ['Page.setViewportSize', { title: 'Set viewport size', snapshot: true, pause: true, }], - ['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, pause: true, }], - ['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, pause: true, }], - ['Page.keyboardInsertText', { title: 'Insert "{text}"', slowMo: true, snapshot: true, pause: true, }], - ['Page.keyboardType', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, }], - ['Page.keyboardPress', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, }], - ['Page.mouseMove', { title: 'Mouse move', slowMo: true, snapshot: true, pause: true, }], - ['Page.mouseDown', { title: 'Mouse down', slowMo: true, snapshot: true, pause: true, }], - ['Page.mouseUp', { title: 'Mouse up', slowMo: true, snapshot: true, pause: true, }], - ['Page.mouseClick', { title: 'Click', slowMo: true, snapshot: true, pause: true, }], - ['Page.mouseWheel', { title: 'Mouse wheel', slowMo: true, snapshot: true, pause: true, }], - ['Page.touchscreenTap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, }], + ['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardInsertText', { title: 'Insert "{text}"', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardType', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardPress', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseMove', { title: 'Mouse move', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseDown', { title: 'Mouse down', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseUp', { title: 'Mouse up', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseClick', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseWheel', { title: 'Mouse wheel', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.touchscreenTap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, }], ['Page.clearPageErrors', { title: 'Clear page errors', }], ['Page.pageErrors', { title: 'Get page errors', group: 'getter', }], ['Page.pdf', { title: 'PDF', }], diff --git a/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts b/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts index 6dbf28e9fcd19..7279ce2b0f178 100644 --- a/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts +++ b/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts @@ -277,8 +277,8 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine win['__playwright_frame_bounding_rects__'] = frameBoundingRectsInfo; const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' + - ' match the center of the clicked element. This is likely due to a difference between' + - ' the test runner and the trace viewer operating systems.'; + ' match the center of the clicked element. This is either due to the use of provided offset,' + + ' or due to a difference between the test runner and the trace viewer operating systems.'; const scrollTops: Element[] = []; const scrollLefts: Element[] = []; @@ -412,54 +412,44 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine const search = new URL(win.location.href).searchParams; const isTopFrame = win === topSnapshotWindow; - if (search.get('pointX') && search.get('pointY')) { + if (isTopFrame && search.get('pointX') && search.get('pointY')) { const pointX = +search.get('pointX')!; const pointY = +search.get('pointY')!; - const hasInputTarget = search.has('hasInputTarget'); - const hasTargetElements = targetElements.length > 0; - const roots = win.document.documentElement ? [win.document.documentElement] : []; - for (const target of (hasTargetElements ? targetElements : roots)) { - const pointElement = win.document.createElement('x-pw-pointer'); - pointElement.style.position = 'fixed'; - pointElement.style.backgroundColor = '#f44336'; - pointElement.style.width = '20px'; - pointElement.style.height = '20px'; - pointElement.style.borderRadius = '10px'; - pointElement.style.margin = '-10px 0 0 -10px'; - pointElement.style.zIndex = '2147483646'; - pointElement.style.display = 'flex'; - pointElement.style.alignItems = 'center'; - pointElement.style.justifyContent = 'center'; - if (hasTargetElements) { - // Sometimes there are layout discrepancies between recording and rendering, e.g. fonts, - // that may place the point at the wrong place. To avoid confusion, we just show the - // point in the middle of the target element. - const box = target.getBoundingClientRect(); - const centerX = (box.left + box.width / 2); - const centerY = (box.top + box.height / 2); - pointElement.style.left = centerX + 'px'; - pointElement.style.top = centerY + 'px'; - // "Warning symbol" indicates that action point is not 100% correct. - // Note that action point is relative to the top frame, so we can only compare in the top frame. - if (isTopFrame && (Math.abs(centerX - pointX) >= 10 || Math.abs(centerY - pointY) >= 10)) { - const warningElement = win.document.createElement('x-pw-pointer-warning'); - warningElement.textContent = '⚠'; - warningElement.style.fontSize = '19px'; - warningElement.style.color = 'white'; - warningElement.style.marginTop = '-3.5px'; - warningElement.style.userSelect = 'none'; - pointElement.appendChild(warningElement); - pointElement.setAttribute('title', kPointerWarningTitle); - } - win.document.documentElement.appendChild(pointElement); - } else if (isTopFrame && !hasInputTarget) { - // For actions without a target element, e.g. page.mouse.move(), - // show the point at the recorded location, which is relative to the top frame. - pointElement.style.left = pointX + 'px'; - pointElement.style.top = pointY + 'px'; - win.document.documentElement.appendChild(pointElement); - } + + const pointElement = win.document.createElement('x-pw-pointer'); + pointElement.style.position = 'fixed'; + pointElement.style.backgroundColor = '#f44336'; + pointElement.style.width = '20px'; + pointElement.style.height = '20px'; + pointElement.style.borderRadius = '10px'; + pointElement.style.margin = '-10px 0 0 -10px'; + pointElement.style.zIndex = '2147483646'; + pointElement.style.display = 'flex'; + pointElement.style.alignItems = 'center'; + pointElement.style.justifyContent = 'center'; + + // Sometimes there are layout discrepancies between recording and rendering, e.g. fonts, + // that may place the point at the wrong place. To avoid confusion, we just show the + // point in the middle of the target element. + const target = targetElements[0]; + const targetBox = target?.getBoundingClientRect(); + const targetCenter = target ? { x: targetBox.left + targetBox.width / 2, y: targetBox.top + targetBox.height / 2 } : null; + pointElement.style.left = (targetCenter?.x ?? pointX) + 'px'; + pointElement.style.top = (targetCenter?.y ?? pointY) + 'px'; + + const isAligned = !targetCenter || (Math.abs(targetCenter.x - pointX) <= 10 && Math.abs(targetCenter.y - pointY) <= 10); + if (!isAligned) { + const warningElement = win.document.createElement('x-pw-pointer-warning'); + warningElement.textContent = '⚠'; + warningElement.style.fontSize = '19px'; + warningElement.style.color = 'white'; + warningElement.style.marginTop = '-3.5px'; + warningElement.style.userSelect = 'none'; + pointElement.appendChild(warningElement); + pointElement.setAttribute('title', kPointerWarningTitle); } + + win.document.documentElement.appendChild(pointElement); } if (canvasElements.length > 0) { diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 2b627388bb323..2fa3d1cf1688a 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -376,6 +376,7 @@ const playwrightFixtures: Fixtures = ({ recordVideo: { dir: tracing().artifactsDir(), size: typeof video === 'string' ? undefined : video.size, + annotate: typeof video === 'string' ? undefined : video.annotate, } } : {}; const context = await browser.newContext({ ...videoOptions, ...options }) as BrowserContextImpl; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index c38e39d3767d5..43f255ee0f574 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6933,6 +6933,9 @@ export interface PlaywrightWorkerOptions { * down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual * picture of each page will be scaled down if necessary to fit the specified size. * + * To annotate actions in the video with element highlights and action title subtitles, pass `annotate` with an + * optional `delay` in milliseconds (defaults to `500`). + * * **Usage** * * ```js @@ -6948,7 +6951,7 @@ export interface PlaywrightWorkerOptions { * * Learn more about [recording video](https://playwright.dev/docs/test-use-options#recording-options). */ - video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize }; + video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize, annotate?: { delay?: number } }; } export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure'; diff --git a/packages/protocol/src/callMetadata.d.ts b/packages/protocol/src/callMetadata.d.ts index e61446d9dc197..ab598f79a226a 100644 --- a/packages/protocol/src/callMetadata.d.ts +++ b/packages/protocol/src/callMetadata.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Point, SerializedError } from './channels'; +import type { Point, Rect, SerializedError } from './channels'; export type CallMetadata = { id: string; @@ -36,9 +36,10 @@ export type CallMetadata = { error?: SerializedError; result?: any; point?: Point; + box? : Rect; objectId?: string; pageId?: string; frameId?: string; potentiallyClosesScope?: boolean; - annotate?: { delay: number }; + annotate?: boolean; }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 2f32f1e7a076e..1cf295abfcf9f 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1847,6 +1847,7 @@ Page: slowMo: true snapshot: true pause: true + input: true keyboardUp: title: Key up "{key}" @@ -1856,6 +1857,7 @@ Page: slowMo: true snapshot: true pause: true + input: true keyboardInsertText: title: Insert "{text}" @@ -1865,6 +1867,7 @@ Page: slowMo: true snapshot: true pause: true + input: true keyboardType: title: Type "{text}" @@ -1875,6 +1878,7 @@ Page: slowMo: true snapshot: true pause: true + input: true keyboardPress: title: Press "{key}" @@ -1885,6 +1889,7 @@ Page: slowMo: true snapshot: true pause: true + input: true mouseMove: title: Mouse move @@ -1896,6 +1901,7 @@ Page: slowMo: true snapshot: true pause: true + input: true mouseDown: title: Mouse down @@ -1911,6 +1917,7 @@ Page: slowMo: true snapshot: true pause: true + input: true mouseUp: title: Mouse up @@ -1926,6 +1933,7 @@ Page: slowMo: true snapshot: true pause: true + input: true mouseClick: title: Click @@ -1944,6 +1952,7 @@ Page: slowMo: true snapshot: true pause: true + input: true mouseWheel: title: Mouse wheel @@ -1954,6 +1963,7 @@ Page: slowMo: true snapshot: true pause: true + input: true touchscreenTap: title: Tap @@ -1964,6 +1974,7 @@ Page: slowMo: true snapshot: true pause: true + input: true clearPageErrors: title: Clear page errors diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 707e8f058a8db..e347c12564f72 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -329,10 +329,9 @@ export type Snapshot = { snapshotName: string; pageId: string; point?: { x: number, y: number }; - hasInputTarget?: boolean; }; -const createSnapshot = (action: ActionTraceEvent, snapshotNameKey: 'beforeSnapshot' | 'afterSnapshot' | 'inputSnapshot', hasInputTarget: boolean = false): Snapshot | undefined => { +const createSnapshot = (action: ActionTraceEvent, snapshotNameKey: 'beforeSnapshot' | 'afterSnapshot' | 'inputSnapshot'): Snapshot | undefined => { if (!action) return undefined; @@ -352,7 +351,6 @@ const createSnapshot = (action: ActionTraceEvent, snapshotNameKey: 'beforeSnapsh snapshotName, pageId: action.pageId, point: action.point, - hasInputTarget, }; }; @@ -413,7 +411,7 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot afterSnapshot = beforeSnapshot; } - const actionSnapshot = createSnapshot(action, 'inputSnapshot', true) ?? afterSnapshot; + const actionSnapshot = createSnapshot(action, 'inputSnapshot') ?? afterSnapshot; if (actionSnapshot) actionSnapshot.point = action.point; return { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot }; @@ -430,8 +428,6 @@ export function extendSnapshot(traceUri: string, snapshot: Snapshot, shouldPopul if (snapshot.point) { params.set('pointX', String(snapshot.point.x)); params.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); } if (shouldPopulateCanvasFromScreenshot) params.set('shouldPopulateCanvasFromScreenshot', '1'); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index f3fda49466892..9e6a77314577c 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1802,9 +1802,9 @@ test('should show only one pointer with multilevel iframes', async ({ page, runA await page.frameLocator('iframe').frameLocator('iframe').locator('button').click({ position: { x: 5, y: 5 } }); }); const snapshotFrame = await traceViewer.snapshotFrame('Click'); - await expect.soft(snapshotFrame.locator('x-pw-pointer')).not.toBeAttached(); + await expect.soft(snapshotFrame.locator('x-pw-pointer')).toBeAttached(); await expect.soft(snapshotFrame.frameLocator('iframe').locator('x-pw-pointer')).not.toBeAttached(); - await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).toBeVisible(); + await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).not.toBeAttached(); }); test('should show a popover', async ({ runAndTrace, page, server, platform, browserName, macVersion }) => { diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index ed8346e485609..fa86e110834b1 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -259,7 +259,7 @@ export interface PlaywrightWorkerOptions { connectOptions: ConnectOptions | undefined; screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick; trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean, screenshots?: boolean, sources?: boolean, attachments?: boolean }; - video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize }; + video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize, annotate?: { delay?: number } }; } export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';