diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index c8f54c7380fac..730269950f1a1 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -234,6 +234,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.click.trial = %%-input-trial-%% * since: v1.11 +### option: ElementHandle.click.steps = %%-input-mousemove-steps-%% +* since: v1.57 + ## async method: ElementHandle.contentFrame * since: v1.8 - returns: <[null]|[Frame]> @@ -287,6 +290,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.dblclick.trial = %%-input-trial-%% * since: v1.11 +### option: ElementHandle.dblclick.steps = %%-input-mousemove-steps-%% +* since: v1.57 + ## async method: ElementHandle.dispatchEvent * since: v1.8 * discouraged: Use locator-based [`method: Locator.dispatchEvent`] instead. Read more about [locators](../locators.md). diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index ac86bae5ab190..4726d6329220a 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -497,6 +497,9 @@ await page.Locator("canvas").ClickAsync(new() { ### option: Locator.click.trial = %%-input-trial-with-modifiers-%% * since: v1.14 +### option: Locator.click.steps = %%-input-mousemove-steps-%% +* since: v1.57 + ## async method: Locator.count * since: v1.14 - returns: <[int]> @@ -580,6 +583,9 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.dblclick.trial = %%-input-trial-with-modifiers-%% * since: v1.14 +### option: Locator.dblclick.steps = %%-input-mousemove-steps-%% +* since: v1.57 + ## method: Locator.describe * since: v1.53 - returns: <[Locator]> diff --git a/docs/src/api/class-mouse.md b/docs/src/api/class-mouse.md index 1897482bac506..179677db5738c 100644 --- a/docs/src/api/class-mouse.md +++ b/docs/src/api/class-mouse.md @@ -143,11 +143,8 @@ X coordinate relative to the main frame's viewport in CSS pixels. Y coordinate relative to the main frame's viewport in CSS pixels. -### option: Mouse.move.steps +### option: Mouse.move.steps = %%-input-mousemove-steps-%% * since: v1.8 -- `steps` <[int]> - -Defaults to 1. Sends intermediate `mousemove` events. ## async method: Mouse.up * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 993b8bc2964e6..e0141bec4436b 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -103,6 +103,11 @@ A selector to search for an element to drop onto. If there are multiple elements A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. +## input-mousemove-steps +- `steps` <[int]> + +Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + ## input-modifiers - `modifiers` <[Array]<[KeyboardModifier]<"Alt"|"Control"|"ControlOrMeta"|"Meta"|"Shift">>> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index df5bfa0bc26ee..f360194364d81 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -11211,6 +11211,12 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -11294,6 +11300,12 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12788,6 +12800,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12905,6 +12923,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -20293,7 +20317,8 @@ export interface Mouse { */ move(x: number, y: number, options?: { /** - * Defaults to 1. Sends intermediate `mousemove` events. + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. */ steps?: number; }): Promise; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 83c27272842d6..540e3488b3736 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1602,6 +1602,7 @@ scheme.FrameClickParams = tObject({ clickCount: tOptional(tInt), timeout: tFloat, trial: tOptional(tBoolean), + steps: tOptional(tInt), }); scheme.FrameClickResult = tOptional(tObject({})); scheme.FrameContentParams = tOptional(tObject({})); @@ -1629,6 +1630,7 @@ scheme.FrameDblclickParams = tObject({ button: tOptional(tEnum(['left', 'right', 'middle'])), timeout: tFloat, trial: tOptional(tBoolean), + steps: tOptional(tInt), }); scheme.FrameDblclickResult = tOptional(tObject({})); scheme.FrameDispatchEventParams = tObject({ @@ -2044,6 +2046,7 @@ scheme.ElementHandleClickParams = tObject({ clickCount: tOptional(tInt), timeout: tFloat, trial: tOptional(tBoolean), + steps: tOptional(tInt), }); scheme.ElementHandleClickResult = tOptional(tObject({})); scheme.ElementHandleContentFrameParams = tOptional(tObject({})); @@ -2058,6 +2061,7 @@ scheme.ElementHandleDblclickParams = tObject({ button: tOptional(tEnum(['left', 'right', 'middle'])), timeout: tFloat, trial: tOptional(tBoolean), + steps: tOptional(tInt), }); scheme.ElementHandleDblclickResult = tOptional(tObject({})); scheme.ElementHandleDispatchEventParams = tObject({ diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index e88c3e09578cf..b1e85186d6b3d 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -215,10 +215,10 @@ export class Mouse { await this._raw.up(progress, this._x, this._y, button, this._buttons, this._keyboard._modifiers(), clickCount); } - async click(progress: Progress, x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}) { - const { delay = null, clickCount = 1 } = 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) { - this.move(progress, x, y, { forClick: true }); + await this.move(progress, x, y, { forClick: true, steps }); for (let cc = 1; cc <= clickCount; ++cc) { await this.down(progress, { ...options, clickCount: cc }); await progress.wait(delay); @@ -228,7 +228,11 @@ export class Mouse { } } else { const promises = []; - promises.push(this.move(progress, x, y, { forClick: true })); + const movePromise = this.move(progress, x, y, { forClick: true, steps }); + if (steps !== undefined && steps > 1) + await movePromise; + else + promises.push(movePromise); for (let cc = 1; cc <= clickCount; ++cc) { promises.push(this.down(progress, { ...options, clickCount: cc })); promises.push(this.up(progress, { ...options, clickCount: cc })); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index df5bfa0bc26ee..f360194364d81 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -11211,6 +11211,12 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -11294,6 +11300,12 @@ export interface ElementHandle extends JSHandle { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12788,6 +12800,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -12905,6 +12923,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -20293,7 +20317,8 @@ export interface Mouse { */ move(x: number, y: number, options?: { /** - * Defaults to 1. Sends intermediate `mousemove` events. + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + * position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. */ steps?: number; }): Promise; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 867b77d13f213..497cbc65824b3 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2803,6 +2803,7 @@ export type FrameClickParams = { clickCount?: number, timeout: number, trial?: boolean, + steps?: number, }; export type FrameClickOptions = { strict?: boolean, @@ -2814,6 +2815,7 @@ export type FrameClickOptions = { button?: 'left' | 'right' | 'middle', clickCount?: number, trial?: boolean, + steps?: number, }; export type FrameClickResult = void; export type FrameContentParams = {}; @@ -2849,6 +2851,7 @@ export type FrameDblclickParams = { button?: 'left' | 'right' | 'middle', timeout: number, trial?: boolean, + steps?: number, }; export type FrameDblclickOptions = { strict?: boolean, @@ -2858,6 +2861,7 @@ export type FrameDblclickOptions = { delay?: number, button?: 'left' | 'right' | 'middle', trial?: boolean, + steps?: number, }; export type FrameDblclickResult = void; export type FrameDispatchEventParams = { @@ -3514,6 +3518,7 @@ export type ElementHandleClickParams = { clickCount?: number, timeout: number, trial?: boolean, + steps?: number, }; export type ElementHandleClickOptions = { force?: boolean, @@ -3524,6 +3529,7 @@ export type ElementHandleClickOptions = { button?: 'left' | 'right' | 'middle', clickCount?: number, trial?: boolean, + steps?: number, }; export type ElementHandleClickResult = void; export type ElementHandleContentFrameParams = {}; @@ -3539,6 +3545,7 @@ export type ElementHandleDblclickParams = { button?: 'left' | 'right' | 'middle', timeout: number, trial?: boolean, + steps?: number, }; export type ElementHandleDblclickOptions = { force?: boolean, @@ -3547,6 +3554,7 @@ export type ElementHandleDblclickOptions = { delay?: number, button?: 'left' | 'right' | 'middle', trial?: boolean, + steps?: number, }; export type ElementHandleDblclickResult = void; export type ElementHandleDispatchEventParams = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index cbb7d1060234a..90c1a84d9d292 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2256,6 +2256,7 @@ Frame: clickCount: int? timeout: float trial: boolean? + steps: int? flags: slowMo: true snapshot: true @@ -2311,6 +2312,7 @@ Frame: - middle timeout: float trial: boolean? + steps: int? flags: slowMo: true snapshot: true @@ -3012,6 +3014,7 @@ ElementHandle: clickCount: int? timeout: float trial: boolean? + steps: int? flags: slowMo: true snapshot: true @@ -3047,6 +3050,7 @@ ElementHandle: - middle timeout: float trial: boolean? + steps: int? flags: slowMo: true snapshot: true diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index b9fa76c3d5ca9..688dc246d05f5 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -1287,3 +1287,33 @@ it('should click shadow root button', { annotation: { type: 'issue', description await page.locator('my-button').click(); }); + +it('should click with tweened mouse movement', async ({ page, browserName, isAndroid }) => { + it.skip(isAndroid, 'Bad rounding'); + + await page.setContent(` + +
Click me
+ + `); + + // The test becomes flaky on WebKit without next line. + if (browserName === 'webkit') + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.mouse.move(100, 100); + await page.evaluate(() => { + window['result'] = []; + document.addEventListener('mousemove', event => { + window['result'].push([event.clientX, event.clientY]); + }); + }); + // Centerpoint at 150 + 100/2, 280 + 40/2 = 200, 300 + await page.locator('div').click({ steps: 5 }); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300] + ]); +});