From 8124a7d5bfc1cfa8cb579271f78ce586efc62b8e Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Tue, 25 Apr 2023 13:28:47 +0200 Subject: [PATCH] fix: implement click `count` (#10069) --- docs/api/puppeteer.clickoptions.md | 13 ++++--- docs/api/puppeteer.frame.click.md | 17 +++------- docs/api/puppeteer.mouse.click.md | 16 +++++---- docs/api/puppeteer.mouseclickoptions.md | 7 ++-- docs/api/puppeteer.mouseoptions.md | 8 ++--- docs/api/puppeteer.page.click.md | 17 +++------- .../puppeteer-core/src/api/ElementHandle.ts | 18 ++-------- packages/puppeteer-core/src/api/Page.ts | 20 +++-------- .../src/common/ElementHandle.ts | 2 +- packages/puppeteer-core/src/common/Frame.ts | 9 ++--- packages/puppeteer-core/src/common/Input.ts | 34 ++++++++++++++----- .../src/common/IsolatedWorld.ts | 5 ++- packages/puppeteer-core/src/common/Page.ts | 10 ++---- test/src/click.spec.ts | 17 +++++++--- 14 files changed, 86 insertions(+), 107 deletions(-) diff --git a/docs/api/puppeteer.clickoptions.md b/docs/api/puppeteer.clickoptions.md index 3f866709..83299f55 100644 --- a/docs/api/puppeteer.clickoptions.md +++ b/docs/api/puppeteer.clickoptions.md @@ -7,14 +7,13 @@ sidebar_label: ClickOptions #### Signature: ```typescript -export interface ClickOptions +export interface ClickOptions extends MouseClickOptions ``` +**Extends:** [MouseClickOptions](./puppeteer.mouseclickoptions.md) + ## Properties -| Property | Modifiers | Type | Description | Default | -| ---------- | --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | -------------- | -| button | optional | [MouseButton](./puppeteer.mousebutton.md) | | 'left' | -| clickCount | optional | number | | 1 | -| delay | optional | number | Time to wait between mousedown and mouseup in milliseconds. | 0 | -| offset | optional | [Offset](./puppeteer.offset.md) | Offset for the clickable point relative to the top-left corner of the border box. | | +| Property | Modifiers | Type | Description | Default | +| -------- | --------------------- | ------------------------------- | --------------------------------------------------------------------------------- | ------- | +| offset | optional | [Offset](./puppeteer.offset.md) | Offset for the clickable point relative to the top-left corner of the border box. | | diff --git a/docs/api/puppeteer.frame.click.md b/docs/api/puppeteer.frame.click.md index 7a916153..ac169ea1 100644 --- a/docs/api/puppeteer.frame.click.md +++ b/docs/api/puppeteer.frame.click.md @@ -10,23 +10,16 @@ Clicks the first element found that matches `selector`. ```typescript class Frame { - click( - selector: string, - options?: { - delay?: number; - button?: MouseButton; - clickCount?: number; - } - ): Promise; + click(selector: string, options?: Readonly): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | -------------------------------------------------------------------------------------------- | -------------------------- | -| selector | string | The selector to query for. | -| options | { delay?: number; button?: [MouseButton](./puppeteer.mousebutton.md); clickCount?: number; } | _(Optional)_ | +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------- | -------------------------- | +| selector | string | The selector to query for. | +| options | Readonly<[ClickOptions](./puppeteer.clickoptions.md)> | _(Optional)_ | **Returns:** diff --git a/docs/api/puppeteer.mouse.click.md b/docs/api/puppeteer.mouse.click.md index 0ad05c0d..f0273bad 100644 --- a/docs/api/puppeteer.mouse.click.md +++ b/docs/api/puppeteer.mouse.click.md @@ -10,17 +10,21 @@ Shortcut for `mouse.move`, `mouse.down` and `mouse.up`. ```typescript class Mouse { - click(x: number, y: number, options?: MouseClickOptions): Promise; + click( + x: number, + y: number, + options?: Readonly + ): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | ----------------------------------------------------- | ------------------------------------------- | -| x | number | Horizontal position of the mouse. | -| y | number | Vertical position of the mouse. | -| options | [MouseClickOptions](./puppeteer.mouseclickoptions.md) | _(Optional)_ Options to configure behavior. | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------------- | ------------------------------------------- | +| x | number | Horizontal position of the mouse. | +| y | number | Vertical position of the mouse. | +| options | Readonly<[MouseClickOptions](./puppeteer.mouseclickoptions.md)> | _(Optional)_ Options to configure behavior. | **Returns:** diff --git a/docs/api/puppeteer.mouseclickoptions.md b/docs/api/puppeteer.mouseclickoptions.md index 971110f2..a8778c17 100644 --- a/docs/api/puppeteer.mouseclickoptions.md +++ b/docs/api/puppeteer.mouseclickoptions.md @@ -14,6 +14,7 @@ export interface MouseClickOptions extends MouseOptions ## Properties -| Property | Modifiers | Type | Description | Default | -| -------- | --------------------- | ------ | -------------------------------------------------------------- | ------- | -| delay | optional | number | Time (in ms) to delay the mouse release after the mouse press. | | +| Property | Modifiers | Type | Description | Default | +| -------- | --------------------- | ------ | -------------------------------------------------------------- | -------------- | +| count | optional | number | Number of clicks to perform. | 1 | +| delay | optional | number | Time (in ms) to delay the mouse release after the mouse press. | | diff --git a/docs/api/puppeteer.mouseoptions.md b/docs/api/puppeteer.mouseoptions.md index 764b355b..71b4b72c 100644 --- a/docs/api/puppeteer.mouseoptions.md +++ b/docs/api/puppeteer.mouseoptions.md @@ -12,7 +12,7 @@ export interface MouseOptions ## Properties -| Property | Modifiers | Type | Description | Default | -| ---------- | --------------------- | ----------------------------------------- | ----------------------------------------- | ------------------- | -| button | optional | [MouseButton](./puppeteer.mousebutton.md) | Determines which button will be pressed. | 'left' | -| clickCount | optional | number | Determines the click count for the mouse. | 1 | +| Property | Modifiers | Type | Description | Default | +| ---------- | --------------------- | ----------------------------------------- | ---------------------------------------- | ------------------- | +| button | optional | [MouseButton](./puppeteer.mousebutton.md) | Determines which button will be pressed. | 'left' | +| clickCount | optional | number | | 1 | diff --git a/docs/api/puppeteer.page.click.md b/docs/api/puppeteer.page.click.md index fd644e8b..ce3aceb6 100644 --- a/docs/api/puppeteer.page.click.md +++ b/docs/api/puppeteer.page.click.md @@ -10,23 +10,16 @@ This method fetches an element with `selector`, scrolls it into view if needed, ```typescript class Page { - click( - selector: string, - options?: { - delay?: number; - button?: MouseButton; - clickCount?: number; - } - ): Promise; + click(selector: string, options?: Readonly): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| selector | string | A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked | -| options | { delay?: number; button?: [MouseButton](./puppeteer.mousebutton.md); clickCount?: number; } | _(Optional)_ Object | +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| selector | string | A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked | +| options | Readonly<[ClickOptions](./puppeteer.clickoptions.md)> | _(Optional)_ Object | **Returns:** diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index a229aa98..09c40973 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -19,7 +19,7 @@ import {Protocol} from 'devtools-protocol'; import {CDPSession} from '../common/Connection.js'; import {ExecutionContext} from '../common/ExecutionContext.js'; import {Frame} from '../common/Frame.js'; -import {MouseButton} from '../common/Input.js'; +import {MouseClickOptions} from '../common/Input.js'; import {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import { ElementFor, @@ -76,21 +76,7 @@ export interface Offset { /** * @public */ -export interface ClickOptions { - /** - * Time to wait between `mousedown` and `mouseup` in milliseconds. - * - * @defaultValue `0` - */ - delay?: number; - /** - * @defaultValue 'left' - */ - button?: MouseButton; - /** - * @defaultValue `1` - */ - clickCount?: number; +export interface ClickOptions extends MouseClickOptions { /** * Offset for the clickable point relative to the top-left corner of the border box. */ diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 6e2dd21d..2bfa7fe5 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -34,20 +34,15 @@ import type { FrameAddStyleTagOptions, FrameWaitForFunctionOptions, } from '../common/Frame.js'; -import type { - Keyboard, - Mouse, - MouseButton, - Touchscreen, -} from '../common/Input.js'; +import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js'; import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js'; import type {Credentials, NetworkConditions} from '../common/NetworkManager.js'; import { LowerCasePaperFormat, + paperFormats, ParsedPDFOptions, PDFOptions, - paperFormats, } from '../common/PDFOptions.js'; import type {Viewport} from '../common/PuppeteerViewport.js'; import type {Target} from '../common/Target.js'; @@ -64,7 +59,7 @@ import {assert} from '../util/assert.js'; import type {Browser} from './Browser.js'; import type {BrowserContext} from './BrowserContext.js'; -import type {ElementHandle} from './ElementHandle.js'; +import type {ClickOptions, ElementHandle} from './ElementHandle.js'; import type {JSHandle} from './JSHandle.js'; /** @@ -2319,14 +2314,7 @@ export class Page extends EventEmitter { * successfully clicked. The Promise will be rejected if there is no element * matching `selector`. */ - click( - selector: string, - options?: { - delay?: number; - button?: MouseButton; - clickCount?: number; - } - ): Promise; + click(selector: string, options?: Readonly): Promise; click(): Promise { throw new Error('Not implemented'); } diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index bfe92a40..351d7057 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -445,7 +445,7 @@ export class CDPElementHandle< */ override async click( this: CDPElementHandle, - options: ClickOptions = {} + options: Readonly = {} ): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(options.offset); diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts index e9cf97f0..10a79c3d 100644 --- a/packages/puppeteer-core/src/common/Frame.ts +++ b/packages/puppeteer-core/src/common/Frame.ts @@ -16,7 +16,7 @@ import {Protocol} from 'devtools-protocol'; -import {ElementHandle} from '../api/ElementHandle.js'; +import {type ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import {HTTPResponse} from '../api/HTTPResponse.js'; import {Page, WaitTimeoutOptions} from '../api/Page.js'; import {assert} from '../util/assert.js'; @@ -30,7 +30,6 @@ import { import {ExecutionContext} from './ExecutionContext.js'; import {FrameManager} from './FrameManager.js'; import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; -import {MouseButton} from './Input.js'; import { IsolatedWorld, IsolatedWorldChart, @@ -944,11 +943,7 @@ export class Frame { */ async click( selector: string, - options: { - delay?: number; - button?: MouseButton; - clickCount?: number; - } = {} + options: Readonly = {} ): Promise { return this.worlds[PUPPETEER_WORLD].click(selector, options); } diff --git a/packages/puppeteer-core/src/common/Input.ts b/packages/puppeteer-core/src/common/Input.ts index b39af4a8..4af29bd5 100644 --- a/packages/puppeteer-core/src/common/Input.ts +++ b/packages/puppeteer-core/src/common/Input.ts @@ -342,7 +342,10 @@ export interface MouseOptions { */ button?: MouseButton; /** - * Determines the click count for the mouse. + * @deprecated Use {@link MouseClickOptions.count}. + * + * Determines the click count for the mouse event. This does not perform + * multiple clicks. * * @defaultValue `1` */ @@ -357,6 +360,12 @@ export interface MouseClickOptions extends MouseOptions { * Time (in ms) to delay the mouse release after the mouse press. */ delay?: number; + /** + * Number of clicks to perform. + * + * @defaultValue `1` + */ + count?: number; } /** @@ -694,15 +703,22 @@ export class Mouse { async click( x: number, y: number, - options: MouseClickOptions = {} + options: Readonly = {} ): Promise { - const {delay} = options; - const actions: Array> = []; - const {position} = this.#state; - if (position.x !== x || position.y !== y) { - actions.push(this.move(x, y)); + const {delay, count = 1, clickCount = count} = options; + if (count < 1) { + throw new Error('Click must occur a positive number of times.'); } - actions.push(this.down(options)); + const actions: Array> = [this.move(x, y)]; + if (clickCount === count) { + for (let i = 1; i < count; ++i) { + actions.push( + this.down({...options, clickCount: i}), + this.up({...options, clickCount: i}) + ); + } + } + actions.push(this.down({...options, clickCount})); if (typeof delay === 'number') { await Promise.all(actions); actions.length = 0; @@ -710,7 +726,7 @@ export class Mouse { setTimeout(resolve, delay); }); } - actions.push(this.up(options)); + actions.push(this.up({...options, clickCount})); await Promise.all(actions); } diff --git a/packages/puppeteer-core/src/common/IsolatedWorld.ts b/packages/puppeteer-core/src/common/IsolatedWorld.ts index 7be1a856..e53fc2ba 100644 --- a/packages/puppeteer-core/src/common/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -16,7 +16,7 @@ import {Protocol} from 'devtools-protocol'; -import type {ElementHandle} from '../api/ElementHandle.js'; +import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import {JSHandle} from '../api/JSHandle.js'; import {assert} from '../util/assert.js'; import {createDeferredPromise} from '../util/DeferredPromise.js'; @@ -26,7 +26,6 @@ import {CDPSession} from './Connection.js'; import {ExecutionContext} from './ExecutionContext.js'; import {Frame} from './Frame.js'; import {FrameManager} from './FrameManager.js'; -import {MouseButton} from './Input.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {TimeoutSettings} from './TimeoutSettings.js'; @@ -306,7 +305,7 @@ export class IsolatedWorld { async click( selector: string, - options: {delay?: number; button?: MouseButton; clickCount?: number} + options: Readonly = {} ): Promise { const handle = await this.$(selector); assert(handle, `No element found for selector: ${selector}`); diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 8fb30ab3..c77b44e6 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -20,7 +20,7 @@ import {Protocol} from 'devtools-protocol'; import type {Browser} from '../api/Browser.js'; import type {BrowserContext} from '../api/BrowserContext.js'; -import {ElementHandle} from '../api/ElementHandle.js'; +import {ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import {HTTPRequest} from '../api/HTTPRequest.js'; import {HTTPResponse} from '../api/HTTPResponse.js'; import {JSHandle} from '../api/JSHandle.js'; @@ -62,7 +62,7 @@ import { FrameWaitForFunctionOptions, } from './Frame.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; -import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js'; +import {Keyboard, Mouse, Touchscreen} from './Input.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {MAIN_WORLD} from './IsolatedWorlds.js'; import { @@ -1551,11 +1551,7 @@ export class CDPPage extends Page { override click( selector: string, - options: { - delay?: number; - button?: MouseButton; - clickCount?: number; - } = {} + options: Readonly = {} ): Promise { return this.mainFrame().click(selector, options); } diff --git a/test/src/click.spec.ts b/test/src/click.spec.ts index 8c1e1cdd..54e7375f 100644 --- a/test/src/click.spec.ts +++ b/test/src/click.spec.ts @@ -155,9 +155,18 @@ describe('Page.click', function () { const text = "This is the text that we are going to try to select. Let's see how it goes."; await page.keyboard.type(text); - await page.click('textarea'); - await page.click('textarea', {clickCount: 2}); - await page.click('textarea', {clickCount: 3}); + await page.evaluate(() => { + (window as any).clicks = []; + window.addEventListener('click', event => { + return (window as any).clicks.push(event.detail); + }); + }); + await page.click('textarea', {count: 3}); + expect( + await page.evaluate(() => { + return (window as any).clicks; + }) + ).toMatchObject({0: 1, 1: 2, 2: 3}); expect( await page.evaluate(() => { const textarea = document.querySelector('textarea'); @@ -328,7 +337,7 @@ describe('Page.click', function () { }); }); const button = (await page.$('button'))!; - await button!.click({clickCount: 2}); + await button!.click({count: 2}); expect(await page.evaluate('double')).toBe(true); expect(await page.evaluate('result')).toBe('Clicked'); });