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');
});