diff --git a/docs/src/api/class-screencast.md b/docs/src/api/class-screencast.md index 1c5f1b4580549..63798eda9ace6 100644 --- a/docs/src/api/class-screencast.md +++ b/docs/src/api/class-screencast.md @@ -40,6 +40,13 @@ If a screencast is already active (e.g. started by tracing or video recording), Defaults to 800×800. +### option: Screencast.start.annotate +* since: v1.59 +- `annotate` ?<[Object]> + - `delay` ?<[int]> How long each annotation is displayed in milliseconds. Defaults to `500`. + +If specified, enables visual annotations on interacted elements during screencast. Interacted elements are highlighted with a semi-transparent blue box and click points are shown as red circles. + ## async method: Screencast.stop * since: v1.59 diff --git a/docs/src/api/class-video.md b/docs/src/api/class-video.md index 945419c3f68c5..aac176b557643 100644 --- a/docs/src/api/class-video.md +++ b/docs/src/api/class-video.md @@ -144,6 +144,13 @@ Path where the video should be saved when the recording is stopped. Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to fit into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. +### option: Video.start.annotate +* since: v1.59 +- `annotate` ?<[Object]> + - `delay` ?<[int]> How long each annotation is displayed in milliseconds. Defaults to `500`. + +If specified, enables visual annotations on interacted elements during video recording. Interacted elements are highlighted with a semi-transparent blue box and click points are shown as red circles. + ## async method: Video.stop * since: v1.59 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 14ab34518b2fe..6e0b59f0ca01a 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -810,6 +810,8 @@ When set to `minimal`, only record information necessary for routing from HAR. T Actual picture of each page will be scaled down if necessary to fit the specified size. - `width` <[int]> Video frame width. - `height` <[int]> Video frame height. + - `annotate` ?<[Object]> If specified, enables visual annotations on interacted elements during video recording. + - `delay` ?<[int]> How long each annotation is displayed in milliseconds. Defaults to `500`. Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. Make sure to await [`method: BrowserContext.close`] for videos to be saved. diff --git a/packages/injected/src/highlight.css b/packages/injected/src/highlight.css index c82132b0ed70a..f4095467f88c4 100644 --- a/packages/injected/src/highlight.css +++ b/packages/injected/src/highlight.css @@ -107,6 +107,11 @@ x-pw-action-point { z-index: 2; } +@keyframes pw-fade-out { + from { opacity: 1; } + to { opacity: 0; } +} + x-pw-separator { height: 1px; margin: 6px 9px; diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index c70cf62e47e48..3319d26056043 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -27,6 +27,8 @@ import type { InjectedScript } from './injectedScript'; type RenderedHighlightEntry = { targetElement: Element, color: string, + borderColor?: string, + fadeDuration?: number, highlightElement: HTMLElement, tooltipElement?: HTMLElement, box?: DOMRect, @@ -38,6 +40,8 @@ type RenderedHighlightEntry = { export type HighlightEntry = { element: Element, color: string, + borderColor?: string, + fadeDuration?: number, tooltipText?: string, }; @@ -123,10 +127,14 @@ export class Highlight { this._glassPaneElement.remove(); } - showActionPoint(x: number, y: number) { + showActionPoint(x: number, y: number, fadeDuration?: number) { this._actionPointElement.style.top = y + 'px'; this._actionPointElement.style.left = x + 'px'; this._actionPointElement.hidden = false; + if (fadeDuration) + this._actionPointElement.style.animation = `pw-fade-out ${fadeDuration}ms ease-out forwards`; + else + this._actionPointElement.style.animation = ''; } hideActionPoint() { @@ -170,7 +178,7 @@ export class Highlight { lineElement.textContent = entry.tooltipText; tooltipElement.appendChild(lineElement); } - this._renderedEntries.push({ targetElement: entry.element, color: entry.color, tooltipElement, highlightElement }); + this._renderedEntries.push({ targetElement: entry.element, color: entry.color, borderColor: entry.borderColor, fadeDuration: entry.fadeDuration, tooltipElement, highlightElement }); } // 2. Trigger layout while positioning tooltips and computing bounding boxes. @@ -198,6 +206,10 @@ export class Highlight { entry.highlightElement.style.width = box.width + 'px'; entry.highlightElement.style.height = box.height + 'px'; entry.highlightElement.style.display = 'block'; + if (entry.borderColor) + entry.highlightElement.style.border = '2px solid ' + entry.borderColor; + if (entry.fadeDuration) + entry.highlightElement.style.animation = `pw-fade-out ${entry.fadeDuration}ms ease-out forwards`; if (this._isUnderTest) console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 89dbad7bdc76e..4f9abc56ac152 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1290,14 +1290,19 @@ export class InjectedScript { } maskSelectors(selectors: ParsedSelector[], color: string) { + const highlight = this._createHighlight(); + const elements = []; + for (const selector of selectors) + elements.push(this.querySelectorAll(selector, this.document.documentElement)); + highlight.maskElements(elements.flat(), color); + } + + private _createHighlight() { if (this._highlight) this.hideHighlight(); this._highlight = new Highlight(this); this._highlight.install(); - const elements = []; - for (const selector of selectors) - elements.push(this.querySelectorAll(selector, this.document.documentElement)); - this._highlight.maskElements(elements.flat(), color); + return this._highlight; } highlight(selector: ParsedSelector) { @@ -1308,6 +1313,23 @@ export class InjectedScript { this._highlight.runHighlightOnRaf(selector); } + highlightNode(node: Node, point?: { x: number, y: number }, delay?: number) { + const highlight = this._createHighlight(); + const fadeDuration = delay ?? 500; + + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + highlight.updateHighlight([{ + element, + color: 'rgba(0, 128, 255, 0.15)', + borderColor: 'rgba(0, 128, 255, 0.6)', + fadeDuration, + }]); + } + if (point) + highlight.showActionPoint(point.x, point.y, fadeDuration); + } + hideHighlight() { if (this._highlight) { this._highlight.uninstall(); diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index f9cc94cca2462..96d78d08cf28c 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10212,6 +10212,16 @@ export interface Browser { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -15485,6 +15495,16 @@ export interface BrowserType { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -17348,6 +17368,16 @@ export interface AndroidDevice { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -19928,6 +19958,16 @@ export interface Electron { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -22027,6 +22067,17 @@ export interface Video { * @param options */ start(options?: { + /** + * If specified, enables visual annotations on interacted elements during video recording. Interacted elements are + * highlighted with a semi-transparent blue box and click points are shown as red circles. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; + /** * Path where the video should be saved when the recording is stopped. */ @@ -22932,6 +22983,16 @@ export interface BrowserContextOptions { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index 335b9046cadcb..6133ca44f0168 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -30,7 +30,7 @@ export class Screencast implements api.Screencast { }); } - async start(onFrame: (frame: { data: Buffer }) => Promise|any, options: { preferredSize?: { width: number, height: number } } = {}): Promise { + async start(onFrame: (frame: { data: Buffer }) => Promise|any, options: { preferredSize?: { width: number, height: number }, annotate?: { delay?: number } } = {}): Promise { if (this._onFrame) throw new Error('Screencast is already started'); this._onFrame = onFrame; diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 8b3a026d4b069..7c0d700dcd696 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -35,8 +35,8 @@ export class Video extends EventEmitter implements api.Video { this._artifact = artifact; } - async start(options: { path?: string, size?: { width: number, height: number } } = {}) { - const result = await this._page._channel.videoStart({ size: options.size }); + async start(options: { path?: string, size?: { width: number, height: number }, annotate?: { delay?: number } } = {}) { + const result = await this._page._channel.videoStart({ size: options.size, annotate: options.annotate }); this._artifact = Artifact.from(result.artifact); this._savePath = options.path; return new DisposableStub(() => this.stop()); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 479b1b2fd426e..639e12fb8ae73 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -611,6 +611,9 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), @@ -722,6 +725,9 @@ scheme.BrowserNewContextParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), @@ -793,6 +799,9 @@ scheme.BrowserNewContextForReuseParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), @@ -911,6 +920,9 @@ scheme.BrowserContextInitializer = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), @@ -1555,6 +1567,9 @@ scheme.PageStartScreencastParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), }); scheme.PageStartScreencastResult = tOptional(tObject({})); scheme.PageStopScreencastParams = tOptional(tObject({})); @@ -1564,6 +1579,9 @@ scheme.PageVideoStartParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), }); scheme.PageVideoStartResult = tObject({ artifact: tChannel(['Artifact']), @@ -2653,6 +2671,9 @@ scheme.ElectronLaunchParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), timezoneId: tOptional(tString), @@ -2888,6 +2909,9 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ width: tInt, height: tInt, })), + annotate: tOptional(tObject({ + delay: tOptional(tInt), + })), })), strictSelectors: tOptional(tBoolean), serviceWorkers: tOptional(tEnum(['allow', 'block'])), diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index a0e8651366ddc..fedf07734e562 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -370,7 +370,7 @@ export class PageDispatcher extends Dispatcher { this._dispatchEvent('screencastFrame', { data: frame.buffer }); }; - await this._page.screencast.startScreencast(this._screencastListener, { quality: 90, width: size.width, height: size.height }); + await this._page.screencast.startScreencast(this._screencastListener, { quality: 90, width: size.width, height: size.height, annotate: params.annotate }); } async stopScreencast(params: channels.PageStopScreencastParams, progress?: Progress): Promise { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index e82f381d565d0..eddf9af75914f 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -377,6 +377,18 @@ 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, @@ -433,7 +445,7 @@ export class ElementHandle extends js.JSHandle { return maybePoint; const point = roundPoint(maybePoint); progress.metadata.point = point; - await progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(progress, point); let hitTargetInterceptionHandle: js.JSHandle | undefined; if (force) { @@ -558,7 +570,7 @@ export class ElementHandle extends js.JSHandle { 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 progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(progress); if (!options.force) progress.log(` waiting for element to be visible and enabled`); const optionsToSelect = [...elements, ...values]; @@ -591,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 progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(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 }]) => { @@ -658,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 progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(progress); if (localPaths || localDirectory) { const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!; await progress.race(Promise.all((localPathsOrDirectory).map(localPath => ( @@ -699,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 progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(progress); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') return result; @@ -715,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 progress.race(this.instrumentation.onBeforeInputAction(this, progress.metadata)); + await this._beforeAction(progress); return this._page.frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => { const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 0bcbb3e9b8c65..270f22ea03208 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -279,7 +279,7 @@ export class Page extends SdkObject { _didClose() { this.frameManager.dispose(); - this.screencast.stopFrameThrottler(); + this.screencast.dispose(); assert(this._closedState !== 'closed', 'Page closed twice'); this._closedState = 'closed'; this.emit(Page.Events.Close); @@ -291,7 +291,7 @@ export class Page extends SdkObject { _didCrash() { this.frameManager.dispose(); - this.screencast.stopFrameThrottler(); + this.screencast.dispose(); this.emit(Page.Events.Crash); this._crashed = true; this.instrumentation.onPageClose(this); diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index 85dc056f8a415..14ef2a6073479 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -23,44 +23,52 @@ import { registry } from './registry'; import { validateVideoSize } from './browserContext'; import type * as types from './types'; +import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; export type ScreencastListener = (frame: types.ScreencastFrame) => void; +export type ScreencastOptions = { width: number, height: number, quality: number, annotate?: types.AnnotateOptions }; -export class Screencast { +export class Screencast implements InstrumentationListener { private _page: Page; private _videoRecorder: VideoRecorder | null = null; private _videoId: string | null = null; - private _listeners = new Set(); + private _clients = new Map(); // Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms. // When throttling for tracing, 200ms between frames, except for 10 frames around the action. - private _frameThrottler = new FrameThrottler(10, 35, 200); + private _frameThrottler: FrameThrottler | undefined; private _videoFrameListener: ScreencastListener | null = null; + private _annotate: types.AnnotateOptions | undefined; constructor(page: Page) { this._page = page; + this._page.instrumentation.addListener(this, page.browserContext); } - stopFrameThrottler() { - this._frameThrottler.dispose(); + dispose() { + this._frameThrottler?.dispose(); + this._frameThrottler = undefined; + this._page.instrumentation.removeListener(this); } startForTracing(listener: ScreencastListener) { this.startScreencast(listener, { width: 800, height: 800, quality: 90 }).catch(e => debugLogger.log('error', e)); - this._frameThrottler.setThrottlingEnabled(true); + this._frameThrottler = new FrameThrottler(10, 35, 200); } stopForTracing(listener: ScreencastListener) { this.stopScreencast(listener).catch(e => debugLogger.log('error', e)); - this._frameThrottler.setThrottlingEnabled(false); + this.dispose(); } throttleFrameAck(ack: () => void) { - // Don't ack immediately, tracing has smart throttling logic that is implemented here. - this._frameThrottler.ack(ack); + if (!this._frameThrottler) + ack(); + else + this._frameThrottler.ack(ack); } temporarilyDisableThrottling() { - this._frameThrottler.recharge(); + this._frameThrottler?.recharge(); } // Note: it is important to start video recorder before sending Screencast.startScreencast, @@ -70,7 +78,10 @@ export class Screencast { if (!recordVideo) return; // validateBrowserContextOptions ensures correct video size. - return this._launchVideoRecorder(recordVideo.dir, recordVideo.size!); + const videoOptions = this._launchVideoRecorder(recordVideo.dir, recordVideo.size!); + if (recordVideo.annotate) + videoOptions.annotate = recordVideo.annotate; + return videoOptions; } private _launchVideoRecorder(dir: string, size: { width: number, height: number }): types.VideoOptions { @@ -102,6 +113,7 @@ export class Screencast { quality: 90, width: options.width, height: options.height, + annotate: options.annotate, }); return this._page.browserContext._browser._videoStarted(this._page, videoId, options.outputFile); } @@ -123,11 +135,13 @@ export class Screencast { video?.reportFinished(); } - async startExplicitVideoRecording(options: { size?: types.Size } = {}) { + async startExplicitVideoRecording(options: { size?: types.Size, annotate?: types.AnnotateOptions } = {}) { if (this._videoId) throw new Error('Video is already being recorded'); const size = validateVideoSize(options.size, this._page.emulatedSize()?.viewport); const videoOptions = this._launchVideoRecorder(this._page.browserContext._browser.options.artifactsDir, size); + if (options.annotate) + videoOptions.annotate = options.annotate; return await this.startVideoRecording(videoOptions); } @@ -137,22 +151,30 @@ export class Screencast { await this.stopVideoRecording(); } - async startScreencast(listener: ScreencastListener, options: { width: number, height: number, quality: number }) { - this._listeners.add(listener); - if (this._listeners.size === 1) + async startScreencast(listener: ScreencastListener, options: ScreencastOptions) { + this._clients.set(listener, options); + if (!this._annotate && options.annotate) + this._annotate = options.annotate; + if (this._clients.size === 1) await this._page.delegate.startScreencast(options); } async stopScreencast(listener: ScreencastListener) { - this._listeners.delete(listener); - if (!this._listeners.size) + this._clients.delete(listener); + if (!this._clients.size) await this._page.delegate.stopScreencast(); + this._annotate = Array.from(this._clients.values()).find(options => options.annotate)?.annotate; } onScreencastFrame(frame: types.ScreencastFrame) { - for (const listener of this._listeners) + for (const listener of this._clients.keys()) listener(frame); } + + async onBeforeInputAction(_: SdkObject, metadata: CallMetadata): Promise { + if (this._annotate) + metadata.annotate = { delay: this._annotate.delay ?? 500 }; + } } class FrameThrottler { diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 1b28efb468602..ddb292a290753 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -43,10 +43,15 @@ export type PointerActionWaitOptions = CommonActionOptions & { trial?: boolean; }; +export type AnnotateOptions = { + delay?: number, +}; + export type VideoOptions = { width: number, height: number, outputFile: string, + annotate?: AnnotateOptions, }; export type ScreencastFrame = { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index f9cc94cca2462..96d78d08cf28c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10212,6 +10212,16 @@ export interface Browser { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -15485,6 +15495,16 @@ export interface BrowserType { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -17348,6 +17368,16 @@ export interface AndroidDevice { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -19928,6 +19958,16 @@ export interface Electron { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** @@ -22027,6 +22067,17 @@ export interface Video { * @param options */ start(options?: { + /** + * If specified, enables visual annotations on interacted elements during video recording. Interacted elements are + * highlighted with a semi-transparent blue box and click points are shown as red circles. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; + /** * Path where the video should be saved when the recording is stopped. */ @@ -22932,6 +22983,16 @@ export interface BrowserContextOptions { */ height: number; }; + + /** + * If specified, enables visual annotations on interacted elements during video recording. + */ + annotate?: { + /** + * How long each annotation is displayed in milliseconds. Defaults to `500`. + */ + delay?: number; + }; }; /** diff --git a/packages/protocol/src/callMetadata.d.ts b/packages/protocol/src/callMetadata.d.ts index f0a26b47ad524..e61446d9dc197 100644 --- a/packages/protocol/src/callMetadata.d.ts +++ b/packages/protocol/src/callMetadata.d.ts @@ -40,4 +40,5 @@ export type CallMetadata = { pageId?: string; frameId?: string; potentiallyClosesScope?: boolean; + annotate?: { delay: number }; }; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 0dc226d6055c1..812c4562c300f 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1027,6 +1027,9 @@ export type BrowserTypeLaunchPersistentContextParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1110,6 +1113,9 @@ export type BrowserTypeLaunchPersistentContextOptions = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1261,6 +1267,9 @@ export type BrowserNewContextParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1329,6 +1338,9 @@ export type BrowserNewContextOptions = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1400,6 +1412,9 @@ export type BrowserNewContextForReuseParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1468,6 +1483,9 @@ export type BrowserNewContextForReuseOptions = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -1603,6 +1621,9 @@ export type BrowserContextInitializer = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -2690,12 +2711,18 @@ export type PageStartScreencastParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }; export type PageStartScreencastOptions = { preferredSize?: { width: number, height: number, }, + annotate?: { + delay?: number, + }, }; export type PageStartScreencastResult = void; export type PageStopScreencastParams = {}; @@ -2706,12 +2733,18 @@ export type PageVideoStartParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }; export type PageVideoStartOptions = { size?: { width: number, height: number, }, + annotate?: { + delay?: number, + }, }; export type PageVideoStartResult = { artifact: ArtifactChannel, @@ -4629,6 +4662,9 @@ export type ElectronLaunchParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, timezoneId?: string, @@ -4665,6 +4701,9 @@ export type ElectronLaunchOptions = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, timezoneId?: string, @@ -5052,6 +5091,9 @@ export type AndroidDeviceLaunchBrowserParams = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', @@ -5118,6 +5160,9 @@ export type AndroidDeviceLaunchBrowserOptions = { width: number, height: number, }, + annotate?: { + delay?: number, + }, }, strictSelectors?: boolean, serviceWorkers?: 'allow' | 'block', diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 90ca47e39ec75..9c4f1e1540b87 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -598,6 +598,10 @@ ContextOptions: properties: width: int height: int + annotate: + type: object? + properties: + delay: int? strictSelectors: boolean? serviceWorkers: type: enum? @@ -2112,6 +2116,10 @@ Page: properties: width: int height: int + annotate: + type: object? + properties: + delay: int? stopScreencast: title: Stop screencast @@ -2126,6 +2134,10 @@ Page: properties: width: int height: int + annotate: + type: object? + properties: + delay: int? returns: artifact: Artifact @@ -4083,6 +4095,10 @@ Electron: properties: width: int height: int + annotate: + type: object? + properties: + delay: int? strictSelectors: boolean? timezoneId: string? tracesDir: string?