From ede43ca2d3a643f85ad7b4deadf2a324f87a2a24 Mon Sep 17 00:00:00 2001
From: jrandolf <101637635+jrandolf@users.noreply.github.com>
Date: Tue, 25 Jul 2023 12:43:07 +0200
Subject: [PATCH] chore: use RxJS for locator implementation (#10607)
Using RxJS greatly simplifies the control flow for locators and comes with automatic cleanup on failure. It greatly simplifies the `signal` logic and the retry logic.
---
docs/api/index.md | 2 +-
docs/api/puppeteer.awaitedlocator.md | 13 +
docs/api/puppeteer.locator.click.md | 2 +-
docs/api/puppeteer.locator.fill.md | 2 +-
docs/api/puppeteer.locator.hover.md | 2 +-
docs/api/puppeteer.locator.md | 30 +-
docs/api/puppeteer.locator.race.md | 6 +-
docs/api/puppeteer.locator.scroll.md | 2 +-
...locator.setensureelementisintheviewport.md | 14 +-
docs/api/puppeteer.locator.settimeout.md | 2 +-
docs/api/puppeteer.locator.setvisibility.md | 8 +-
.../puppeteer.locator.setwaitforenabled.md | 14 +-
...eer.locator.setwaitforstableboundingbox.md | 14 +-
docs/api/puppeteer.unionlocatorof.md | 13 -
package-lock.json | 409 +++++++------
package.json | 2 +-
packages/puppeteer-core/package.json | 1 +
.../rollup.third_party.config.mjs | 4 +-
packages/puppeteer-core/src/api/Page.ts | 8 +-
.../src/api/locators/ExpectedLocator.ts | 145 +++--
.../src/api/locators/Locator.ts | 576 ++++++++++++++++--
.../src/api/locators/NodeLocator.ts | 518 ++--------------
.../src/api/locators/RaceLocator.ts | 199 ++----
.../src/api/locators/locators.ts | 16 +
.../puppeteer-core/third_party/rxjs/rxjs.ts | 41 ++
test/src/locator.spec.ts | 11 +-
26 files changed, 1083 insertions(+), 971 deletions(-)
create mode 100644 docs/api/puppeteer.awaitedlocator.md
delete mode 100644 docs/api/puppeteer.unionlocatorof.md
create mode 100644 packages/puppeteer-core/third_party/rxjs/rxjs.ts
diff --git a/docs/api/index.md b/docs/api/index.md
index a30c402e488..0700b872cee 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -153,6 +153,7 @@ sidebar_label: API
| [ActionResult](./puppeteer.actionresult.md) | |
| [Awaitable](./puppeteer.awaitable.md) | |
| [AwaitableIterable](./puppeteer.awaitableiterable.md) | |
+| [AwaitedLocator](./puppeteer.awaitedlocator.md) | |
| [ChromeReleaseChannel](./puppeteer.chromereleasechannel.md) | |
| [ConsoleMessageType](./puppeteer.consolemessagetype.md) | The supported types for console messages. |
| [ElementFor](./puppeteer.elementfor.md) | |
@@ -182,5 +183,4 @@ sidebar_label: API
| [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | Utility type exposed to enable users to define options that can be passed to puppeteer.launch
without having to list the set of all types. |
| [ResourceType](./puppeteer.resourcetype.md) | Resource types for HTTPRequests as perceived by the rendering engine. |
| [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | |
-| [UnionLocatorOf](./puppeteer.unionlocatorof.md) | |
| [VisibilityOption](./puppeteer.visibilityoption.md) | |
diff --git a/docs/api/puppeteer.awaitedlocator.md b/docs/api/puppeteer.awaitedlocator.md
new file mode 100644
index 00000000000..fd2f4a18e09
--- /dev/null
+++ b/docs/api/puppeteer.awaitedlocator.md
@@ -0,0 +1,13 @@
+---
+sidebar_label: AwaitedLocator
+---
+
+# AwaitedLocator type
+
+#### Signature:
+
+```typescript
+export type AwaitedLocator = T extends Locator ? S : never;
+```
+
+**References:** [Locator](./puppeteer.locator.md)
diff --git a/docs/api/puppeteer.locator.click.md b/docs/api/puppeteer.locator.click.md
index 0b0cce00774..35a903680ac 100644
--- a/docs/api/puppeteer.locator.click.md
+++ b/docs/api/puppeteer.locator.click.md
@@ -8,7 +8,7 @@ sidebar_label: Locator.click
```typescript
class Locator {
- abstract click(
+ click(
this: Locator,
options?: Readonly
): Promise;
diff --git a/docs/api/puppeteer.locator.fill.md b/docs/api/puppeteer.locator.fill.md
index 3f622bbd684..784020b293c 100644
--- a/docs/api/puppeteer.locator.fill.md
+++ b/docs/api/puppeteer.locator.fill.md
@@ -10,7 +10,7 @@ Fills out the input identified by the locator using the provided value. The type
```typescript
class Locator {
- abstract fill(
+ fill(
this: Locator,
value: string,
options?: Readonly
diff --git a/docs/api/puppeteer.locator.hover.md b/docs/api/puppeteer.locator.hover.md
index b1ece20715c..583193117ed 100644
--- a/docs/api/puppeteer.locator.hover.md
+++ b/docs/api/puppeteer.locator.hover.md
@@ -8,7 +8,7 @@ sidebar_label: Locator.hover
```typescript
class Locator {
- abstract hover(
+ hover(
this: Locator,
options?: Readonly
): Promise;
diff --git a/docs/api/puppeteer.locator.md b/docs/api/puppeteer.locator.md
index 9adeefa5bdf..96574f90ba9 100644
--- a/docs/api/puppeteer.locator.md
+++ b/docs/api/puppeteer.locator.md
@@ -22,18 +22,18 @@ export declare abstract class Locator extends EventEmitter
## Methods
-| Method | Modifiers | Description |
-| ------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [click(this, options)](./puppeteer.locator.click.md) | | |
-| [fill(this, value, options)](./puppeteer.locator.fill.md) | | Fills out the input identified by the locator using the provided value. The type of the input is determined at runtime and the appropriate fill-out method is chosen based on the type. contenteditable, selector, inputs are supported. |
-| [hover(this, options)](./puppeteer.locator.hover.md) | | |
-| [off(eventName, handler)](./puppeteer.locator.off.md) | | |
-| [on(eventName, handler)](./puppeteer.locator.on.md) | | |
-| [once(eventName, handler)](./puppeteer.locator.once.md) | | |
-| [race(locators)](./puppeteer.locator.race.md) | static
| Creates a race between multiple locators but ensures that only a single one acts. |
-| [scroll(this, options)](./puppeteer.locator.scroll.md) | | |
-| [setEnsureElementIsInTheViewport(value)](./puppeteer.locator.setensureelementisintheviewport.md) | | |
-| [setTimeout(timeout)](./puppeteer.locator.settimeout.md) | | |
-| [setVisibility(visibility)](./puppeteer.locator.setvisibility.md) | | |
-| [setWaitForEnabled(value)](./puppeteer.locator.setwaitforenabled.md) | | |
-| [setWaitForStableBoundingBox(value)](./puppeteer.locator.setwaitforstableboundingbox.md) | | |
+| Method | Modifiers | Description |
+| ------------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [click(this, options)](./puppeteer.locator.click.md) | | |
+| [fill(this, value, options)](./puppeteer.locator.fill.md) | | Fills out the input identified by the locator using the provided value. The type of the input is determined at runtime and the appropriate fill-out method is chosen based on the type. contenteditable, selector, inputs are supported. |
+| [hover(this, options)](./puppeteer.locator.hover.md) | | |
+| [off(eventName, handler)](./puppeteer.locator.off.md) | | |
+| [on(eventName, handler)](./puppeteer.locator.on.md) | | |
+| [once(eventName, handler)](./puppeteer.locator.once.md) | | |
+| [race(locators)](./puppeteer.locator.race.md) | static
| Creates a race between multiple locators but ensures that only a single one acts. |
+| [scroll(this, options)](./puppeteer.locator.scroll.md) | | |
+| [setEnsureElementIsInTheViewport(this, value)](./puppeteer.locator.setensureelementisintheviewport.md) | | |
+| [setTimeout(timeout)](./puppeteer.locator.settimeout.md) | | |
+| [setVisibility(this, visibility)](./puppeteer.locator.setvisibility.md) | | |
+| [setWaitForEnabled(this, value)](./puppeteer.locator.setwaitforenabled.md) | | |
+| [setWaitForStableBoundingBox(this, value)](./puppeteer.locator.setwaitforstableboundingbox.md) | | |
diff --git a/docs/api/puppeteer.locator.race.md b/docs/api/puppeteer.locator.race.md
index 642df259b9c..8e285164aee 100644
--- a/docs/api/puppeteer.locator.race.md
+++ b/docs/api/puppeteer.locator.race.md
@@ -10,9 +10,9 @@ Creates a race between multiple locators but ensures that only a single one acts
```typescript
class Locator {
- static race>>(
+ static race(
locators: Locators
- ): Locator>;
+ ): Locator>;
}
```
@@ -24,4 +24,4 @@ class Locator {
**Returns:**
-[Locator](./puppeteer.locator.md)<[UnionLocatorOf](./puppeteer.unionlocatorof.md)<Locators>>
+[Locator](./puppeteer.locator.md)<[AwaitedLocator](./puppeteer.awaitedlocator.md)<Locators\[number\]>>
diff --git a/docs/api/puppeteer.locator.scroll.md b/docs/api/puppeteer.locator.scroll.md
index ca58be382cf..d7f4ababe35 100644
--- a/docs/api/puppeteer.locator.scroll.md
+++ b/docs/api/puppeteer.locator.scroll.md
@@ -8,7 +8,7 @@ sidebar_label: Locator.scroll
```typescript
class Locator {
- abstract scroll(
+ scroll(
this: Locator,
options?: Readonly
): Promise;
diff --git a/docs/api/puppeteer.locator.setensureelementisintheviewport.md b/docs/api/puppeteer.locator.setensureelementisintheviewport.md
index 37de38bdcb5..65d3179ed64 100644
--- a/docs/api/puppeteer.locator.setensureelementisintheviewport.md
+++ b/docs/api/puppeteer.locator.setensureelementisintheviewport.md
@@ -8,16 +8,20 @@ sidebar_label: Locator.setEnsureElementIsInTheViewport
```typescript
class Locator {
- abstract setEnsureElementIsInTheViewport(value: boolean): this;
+ setEnsureElementIsInTheViewport(
+ this: Locator,
+ value: boolean
+ ): Locator;
}
```
## Parameters
-| Parameter | Type | Description |
-| --------- | ------- | ----------- |
-| value | boolean | |
+| Parameter | Type | Description |
+| --------- | ---------------------------------------------------- | ----------- |
+| this | [Locator](./puppeteer.locator.md)<ElementType> | |
+| value | boolean | |
**Returns:**
-this
+[Locator](./puppeteer.locator.md)<ElementType>
diff --git a/docs/api/puppeteer.locator.settimeout.md b/docs/api/puppeteer.locator.settimeout.md
index da7508c93df..33099941c66 100644
--- a/docs/api/puppeteer.locator.settimeout.md
+++ b/docs/api/puppeteer.locator.settimeout.md
@@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
```typescript
class Locator {
- abstract setTimeout(timeout: number): this;
+ setTimeout(timeout: number): this;
}
```
diff --git a/docs/api/puppeteer.locator.setvisibility.md b/docs/api/puppeteer.locator.setvisibility.md
index 1cab9ec8a28..0ee6dc6fb29 100644
--- a/docs/api/puppeteer.locator.setvisibility.md
+++ b/docs/api/puppeteer.locator.setvisibility.md
@@ -8,7 +8,10 @@ sidebar_label: Locator.setVisibility
```typescript
class Locator {
- abstract setVisibility(visibility: VisibilityOption): this;
+ setVisibility(
+ this: Locator,
+ visibility: VisibilityOption
+ ): Locator;
}
```
@@ -16,8 +19,9 @@ class Locator {
| Parameter | Type | Description |
| ---------- | --------------------------------------------------- | ----------- |
+| this | [Locator](./puppeteer.locator.md)<NodeType> | |
| visibility | [VisibilityOption](./puppeteer.visibilityoption.md) | |
**Returns:**
-this
+[Locator](./puppeteer.locator.md)<NodeType>
diff --git a/docs/api/puppeteer.locator.setwaitforenabled.md b/docs/api/puppeteer.locator.setwaitforenabled.md
index 3b097f4023c..5b23898fb61 100644
--- a/docs/api/puppeteer.locator.setwaitforenabled.md
+++ b/docs/api/puppeteer.locator.setwaitforenabled.md
@@ -8,16 +8,20 @@ sidebar_label: Locator.setWaitForEnabled
```typescript
class Locator {
- abstract setWaitForEnabled(value: boolean): this;
+ setWaitForEnabled(
+ this: Locator,
+ value: boolean
+ ): Locator;
}
```
## Parameters
-| Parameter | Type | Description |
-| --------- | ------- | ----------- |
-| value | boolean | |
+| Parameter | Type | Description |
+| --------- | ------------------------------------------------- | ----------- |
+| this | [Locator](./puppeteer.locator.md)<NodeType> | |
+| value | boolean | |
**Returns:**
-this
+[Locator](./puppeteer.locator.md)<NodeType>
diff --git a/docs/api/puppeteer.locator.setwaitforstableboundingbox.md b/docs/api/puppeteer.locator.setwaitforstableboundingbox.md
index f76323a1799..fd0141aaf3a 100644
--- a/docs/api/puppeteer.locator.setwaitforstableboundingbox.md
+++ b/docs/api/puppeteer.locator.setwaitforstableboundingbox.md
@@ -8,16 +8,20 @@ sidebar_label: Locator.setWaitForStableBoundingBox
```typescript
class Locator {
- abstract setWaitForStableBoundingBox(value: boolean): this;
+ setWaitForStableBoundingBox(
+ this: Locator,
+ value: boolean
+ ): Locator;
}
```
## Parameters
-| Parameter | Type | Description |
-| --------- | ------- | ----------- |
-| value | boolean | |
+| Parameter | Type | Description |
+| --------- | ---------------------------------------------------- | ----------- |
+| this | [Locator](./puppeteer.locator.md)<ElementType> | |
+| value | boolean | |
**Returns:**
-this
+[Locator](./puppeteer.locator.md)<ElementType>
diff --git a/docs/api/puppeteer.unionlocatorof.md b/docs/api/puppeteer.unionlocatorof.md
deleted file mode 100644
index ea442953b4d..00000000000
--- a/docs/api/puppeteer.unionlocatorof.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-sidebar_label: UnionLocatorOf
----
-
-# UnionLocatorOf type
-
-#### Signature:
-
-```typescript
-export type UnionLocatorOf = T extends Array> ? S : never;
-```
-
-**References:** [Locator](./puppeteer.locator.md)
diff --git a/package-lock.json b/package-lock.json
index 5bf156669bf..c05de4ba9f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,8 +18,8 @@
"@microsoft/api-extractor-model": "7.27.4",
"@pptr/testserver": "file:packages/testserver",
"@prettier/sync": "0.2.1",
- "@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.1.0",
+ "@rollup/plugin-terser": "0.4.3",
"@types/debug": "4.1.8",
"@types/diff": "5.0.3",
"@types/mime": "3.0.1",
@@ -1528,6 +1528,20 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"dev": true,
@@ -1536,10 +1550,39 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"license": "MIT"
},
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.18",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
+ "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
"node_modules/@microsoft/api-documenter": {
"version": "7.22.27",
"resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.27.tgz",
@@ -1935,76 +1978,6 @@
"resolved": "packages/ng-schematics",
"link": true
},
- "node_modules/@rollup/plugin-commonjs": {
- "version": "25.0.2",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.2.tgz",
- "integrity": "sha512-NGTwaJxIO0klMs+WSFFtBP7b9TdTJ3K76HZkewT8/+yHzMiUGVQgaPtLQxNVYIgT5F7lxkEyVID+yS3K7bhCow==",
- "dev": true,
- "dependencies": {
- "@rollup/pluginutils": "^5.0.1",
- "commondir": "^1.0.1",
- "estree-walker": "^2.0.2",
- "glob": "^8.0.3",
- "is-reference": "1.2.1",
- "magic-string": "^0.27.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "rollup": "^2.68.0||^3.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
- "version": "2.0.2",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/glob": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
- "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
- "dev": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^5.0.1",
- "once": "^1.3.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz",
@@ -2035,6 +2008,37 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@rollup/plugin-terser": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz",
+ "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==",
+ "dev": true,
+ "dependencies": {
+ "serialize-javascript": "^6.0.1",
+ "smob": "^1.0.0",
+ "terser": "^5.17.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.x || ^3.x"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/@rollup/pluginutils": {
"version": "5.0.2",
"dev": true,
@@ -3529,11 +3533,6 @@
"node": "^12.20.0 || >=14"
}
},
- "node_modules/commondir": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT"
- },
"node_modules/commonmark": {
"version": "0.30.0",
"dev": true,
@@ -6293,14 +6292,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-reference": {
- "version": "1.2.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "*"
- }
- },
"node_modules/is-regex": {
"version": "1.1.4",
"dev": true,
@@ -6885,17 +6876,6 @@
"node": ">=10"
}
},
- "node_modules/magic-string": {
- "version": "0.27.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.13"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/make-dir": {
"version": "3.1.0",
"dev": true,
@@ -9344,6 +9324,12 @@
"npm": ">= 3.0.0"
}
},
+ "node_modules/smob": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz",
+ "integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==",
+ "dev": true
+ },
"node_modules/socks": {
"version": "2.7.1",
"license": "MIT",
@@ -9761,6 +9747,30 @@
"node": ">=8"
}
},
+ "node_modules/terser": {
+ "version": "5.19.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
+ "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
"node_modules/test-exclude": {
"version": "6.0.0",
"dev": true,
@@ -10148,15 +10158,6 @@
"node": ">=10.12.0"
}
},
- "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.16",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "3.1.0",
- "@jridgewell/sourcemap-codec": "1.4.14"
- }
- },
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"dev": true,
@@ -10570,12 +10571,28 @@
},
"devDependencies": {
"mitt": "3.0.0",
- "parsel-js": "1.1.0"
+ "parsel-js": "1.1.0",
+ "rxjs": "7.8.1"
},
"engines": {
"node": ">=16.3.0"
}
},
+ "packages/puppeteer-core/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "packages/puppeteer-core/node_modules/tslib": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+ "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==",
+ "dev": true
+ },
"packages/testserver": {
"name": "@pptr/testserver",
"version": "0.6.0",
@@ -11463,13 +11480,50 @@
"chalk": "^4.0.0"
}
},
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"dev": true
},
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true
+ },
+ "@jridgewell/source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
"@jridgewell/sourcemap-codec": {
"version": "1.4.14"
},
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.18",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
+ "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
"@microsoft/api-documenter": {
"version": "7.22.27",
"resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.27.tgz",
@@ -11823,57 +11877,6 @@
}
}
},
- "@rollup/plugin-commonjs": {
- "version": "25.0.2",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.2.tgz",
- "integrity": "sha512-NGTwaJxIO0klMs+WSFFtBP7b9TdTJ3K76HZkewT8/+yHzMiUGVQgaPtLQxNVYIgT5F7lxkEyVID+yS3K7bhCow==",
- "dev": true,
- "requires": {
- "@rollup/pluginutils": "^5.0.1",
- "commondir": "^1.0.1",
- "estree-walker": "^2.0.2",
- "glob": "^8.0.3",
- "is-reference": "1.2.1",
- "magic-string": "^0.27.0"
- },
- "dependencies": {
- "brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0"
- }
- },
- "estree-walker": {
- "version": "2.0.2",
- "dev": true
- },
- "glob": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
- "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^5.0.1",
- "once": "^1.3.0"
- }
- },
- "minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
- "dev": true,
- "requires": {
- "brace-expansion": "^2.0.1"
- }
- }
- }
- },
"@rollup/plugin-node-resolve": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz",
@@ -11894,6 +11897,28 @@
}
}
},
+ "@rollup/plugin-terser": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz",
+ "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==",
+ "dev": true,
+ "requires": {
+ "serialize-javascript": "^6.0.1",
+ "smob": "^1.0.0",
+ "terser": "^5.17.4"
+ },
+ "dependencies": {
+ "serialize-javascript": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ }
+ }
+ },
"@rollup/pluginutils": {
"version": "5.0.2",
"dev": true,
@@ -12938,10 +12963,6 @@
"dev": true,
"optional": true
},
- "commondir": {
- "version": "1.0.1",
- "dev": true
- },
"commonmark": {
"version": "0.30.0",
"dev": true,
@@ -14726,13 +14747,6 @@
"version": "1.1.0",
"dev": true
},
- "is-reference": {
- "version": "1.2.1",
- "dev": true,
- "requires": {
- "@types/estree": "*"
- }
- },
"is-regex": {
"version": "1.1.4",
"dev": true,
@@ -15152,13 +15166,6 @@
"yallist": "^4.0.0"
}
},
- "magic-string": {
- "version": "0.27.0",
- "dev": true,
- "requires": {
- "@jridgewell/sourcemap-codec": "^1.4.13"
- }
- },
"make-dir": {
"version": "3.1.0",
"dev": true,
@@ -16337,7 +16344,25 @@
"devtools-protocol": "0.0.1147663",
"mitt": "3.0.0",
"parsel-js": "1.1.0",
+ "rxjs": "7.8.1",
"ws": "8.13.0"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "tslib": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+ "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==",
+ "dev": true
+ }
}
},
"queue-microtask": {
@@ -16845,6 +16870,12 @@
"smart-buffer": {
"version": "4.2.0"
},
+ "smob": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz",
+ "integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==",
+ "dev": true
+ },
"socks": {
"version": "2.7.1",
"requires": {
@@ -17155,6 +17186,26 @@
"streamx": "^2.15.0"
}
},
+ "terser": {
+ "version": "5.19.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
+ "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ }
+ }
+ },
"test-exclude": {
"version": "6.0.0",
"dev": true,
@@ -17420,16 +17471,6 @@
"@jridgewell/trace-mapping": "^0.3.12",
"@types/istanbul-lib-coverage": "^2.0.1",
"convert-source-map": "^1.6.0"
- },
- "dependencies": {
- "@jridgewell/trace-mapping": {
- "version": "0.3.16",
- "dev": true,
- "requires": {
- "@jridgewell/resolve-uri": "3.1.0",
- "@jridgewell/sourcemap-codec": "1.4.14"
- }
- }
}
},
"validate-npm-package-license": {
diff --git a/package.json b/package.json
index 29385ceb23b..7fd74e8d0df 100644
--- a/package.json
+++ b/package.json
@@ -110,8 +110,8 @@
"@microsoft/api-extractor-model": "7.27.4",
"@pptr/testserver": "file:packages/testserver",
"@prettier/sync": "0.2.1",
- "@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.1.0",
+ "@rollup/plugin-terser": "0.4.3",
"@types/debug": "4.1.8",
"@types/diff": "5.0.3",
"@types/mime": "3.0.1",
diff --git a/packages/puppeteer-core/package.json b/packages/puppeteer-core/package.json
index a978ce2fb5b..c39bfaa1304 100644
--- a/packages/puppeteer-core/package.json
+++ b/packages/puppeteer-core/package.json
@@ -152,6 +152,7 @@
"@puppeteer/browsers": "1.4.6"
},
"devDependencies": {
+ "rxjs": "7.8.1",
"mitt": "3.0.0",
"parsel-js": "1.1.0"
}
diff --git a/packages/puppeteer-core/rollup.third_party.config.mjs b/packages/puppeteer-core/rollup.third_party.config.mjs
index 36b83c079be..b9ae06b58d3 100644
--- a/packages/puppeteer-core/rollup.third_party.config.mjs
+++ b/packages/puppeteer-core/rollup.third_party.config.mjs
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import commonjs from '@rollup/plugin-commonjs';
import {nodeResolve} from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
import {globSync} from 'glob';
export default ['cjs', 'esm'].flatMap(outputType => {
@@ -28,7 +28,7 @@ export default ['cjs', 'esm'].flatMap(outputType => {
file,
format: outputType,
},
- plugins: [commonjs(), nodeResolve()],
+ plugins: [terser(), nodeResolve()],
});
}
return configs;
diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts
index 2b0fadeb164..9629210a359 100644
--- a/packages/puppeteer-core/src/api/Page.ts
+++ b/packages/puppeteer-core/src/api/Page.ts
@@ -73,7 +73,7 @@ import type {
} from './Frame.js';
import {Keyboard, KeyboardTypeOptions, Mouse, Touchscreen} from './Input.js';
import type {JSHandle} from './JSHandle.js';
-import {Locator, NodeLocator, UnionLocatorOf} from './locators/locators.js';
+import {AwaitedLocator, Locator, NodeLocator} from './locators/locators.js';
import type {Target} from './Target.js';
/**
@@ -850,10 +850,10 @@ export class Page extends EventEmitter {
*
* @internal
*/
- locatorRace>>(
+ locatorRace(
locators: Locators
- ): Locator> {
- return Locator.race(locators as Array>>);
+ ): Locator> {
+ return Locator.race(locators);
}
/**
diff --git a/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
index b11571a0482..b0a8bc6b122 100644
--- a/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
+++ b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
@@ -1,16 +1,30 @@
-import {Awaitable} from '../../common/common.js';
-import {ElementHandle} from '../ElementHandle.js';
+/**
+ * Copyright 2023 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import {
- ActionOptions,
- LOCATOR_CONTEXTS,
- Locator,
- LocatorClickOptions,
- LocatorContext,
- LocatorScrollOptions,
- VisibilityOption,
- type ActionCondition,
-} from './locators.js';
+ Observable,
+ from,
+ map,
+ mergeMap,
+ throwIfEmpty,
+} from '../../../third_party/rxjs/rxjs.js';
+import {Awaitable, HandleFor} from '../../common/common.js';
+import {ElementHandle} from '../ElementHandle.js';
+
+import {ActionOptions, Locator, VisibilityOption} from './locators.js';
/**
* @public
@@ -31,76 +45,75 @@ export class ExpectedLocator extends Locator {
this.#base = base;
this.#predicate = predicate;
+
+ this.copyOptions(this.#base);
}
- override setVisibility(visibility: VisibilityOption): this {
- this.#base.setVisibility(visibility);
- return this;
- }
override setTimeout(timeout: number): this {
+ super.setTimeout(timeout);
this.#base.setTimeout(timeout);
return this;
}
- override setEnsureElementIsInTheViewport(value: boolean): this {
- this.#base.setEnsureElementIsInTheViewport(value);
+
+ override setVisibility(
+ this: ExpectedLocator,
+ visibility: VisibilityOption
+ ): Locator {
+ super.setVisibility(visibility);
+ this.#base.setVisibility(visibility);
return this;
}
- override setWaitForEnabled(value: boolean): this {
+
+ override setWaitForEnabled(
+ this: ExpectedLocator,
+ value: boolean
+ ): Locator {
+ super.setWaitForEnabled(value);
this.#base.setWaitForEnabled(value);
return this;
}
- override setWaitForStableBoundingBox(value: boolean): this {
+
+ override setEnsureElementIsInTheViewport<
+ FromElement extends Element,
+ ToElement extends FromElement,
+ >(
+ this: ExpectedLocator,
+ value: boolean
+ ): Locator {
+ super.setEnsureElementIsInTheViewport(value);
+ this.#base.setEnsureElementIsInTheViewport(value);
+ return this;
+ }
+
+ override setWaitForStableBoundingBox<
+ FromElement extends Element,
+ ToElement extends FromElement,
+ >(
+ this: ExpectedLocator,
+ value: boolean
+ ): Locator {
+ super.setWaitForStableBoundingBox(value);
this.#base.setWaitForStableBoundingBox(value);
return this;
}
- #condition: ActionCondition = async (handle, signal) => {
- // TODO(jrandolf): We should remove this once JSHandle has waitForFunction.
- await (handle as ElementHandle).frame.waitForFunction(
- this.#predicate,
- {signal},
- handle
+ override _wait(options?: Readonly): Observable> {
+ return this.#base._wait(options).pipe(
+ mergeMap(handle => {
+ return from(
+ (handle as ElementHandle).frame.waitForFunction(
+ this.#predicate,
+ {signal: options?.signal, timeout: this.timeout},
+ handle
+ )
+ ).pipe(
+ map(() => {
+ // SAFETY: It passed the predicate, so this is correct.
+ return handle as HandleFor;
+ })
+ );
+ }),
+ throwIfEmpty()
);
- };
-
- #insertFilterCondition<
- FromElement extends Node,
- ToElement extends FromElement,
- >(this: ExpectedLocator): void {
- const context = (LOCATOR_CONTEXTS.get(this.#base) ??
- {}) as LocatorContext;
- context.conditions ??= new Set();
- context.conditions.add(this.#condition);
- LOCATOR_CONTEXTS.set(this.#base, context);
- }
-
- override click(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.click(options);
- }
- override fill(
- this: ExpectedLocator,
- value: string,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.fill(value, options);
- }
- override hover(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.hover(options);
- }
- override scroll(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.scroll(options);
}
}
diff --git a/packages/puppeteer-core/src/api/locators/Locator.ts b/packages/puppeteer-core/src/api/locators/Locator.ts
index 3d8e99f0c61..94d268e086f 100644
--- a/packages/puppeteer-core/src/api/locators/Locator.ts
+++ b/packages/puppeteer-core/src/api/locators/Locator.ts
@@ -1,28 +1,67 @@
-import {EventEmitter} from '../../common/EventEmitter.js';
-import {ClickOptions} from '../ElementHandle.js';
+/**
+ * Copyright 2023 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import {
+ EMPTY,
+ Observable,
+ OperatorFunction,
+ catchError,
+ defaultIfEmpty,
+ defer,
+ filter,
+ first,
+ firstValueFrom,
+ from,
+ fromEvent,
+ identity,
+ ignoreElements,
+ map,
+ merge,
+ mergeMap,
+ noop,
+ pipe,
+ raceWith,
+ retry,
+ tap,
+ timer,
+} from '../../../third_party/rxjs/rxjs.js';
+import {TimeoutError} from '../../common/Errors.js';
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {HandleFor} from '../../common/types.js';
+import {debugError} from '../../common/util.js';
+import {BoundingBox, ClickOptions, ElementHandle} from '../ElementHandle.js';
+
+import {
+ Action,
+ AwaitedLocator,
ExpectedLocator,
Predicate,
RaceLocator,
- UnionLocatorOf,
- type ActionCondition,
} from './locators.js';
/**
+ * For observables coming from promises, a delay is needed, otherwise RxJS will
+ * never yield in a permanent failure for a promise.
+ *
+ * We also don't want RxJS to do promise operations to often, so we bump the
+ * delay up to 100ms.
+ *
* @internal
*/
-export interface LocatorContext {
- conditions?: Set>;
-}
-
-/**
- * @internal
- */
-export const LOCATOR_CONTEXTS = new WeakMap<
- Locator,
- LocatorContext
->();
+export const RETRY_DELAY = 100;
/**
* @public
@@ -113,33 +152,81 @@ export interface LocatorEventObject {
* @public
*/
export abstract class Locator extends EventEmitter {
+ /**
+ * Creates a race between multiple locators but ensures that only a single one
+ * acts.
+ *
+ * @public
+ */
+ static race(
+ locators: Locators
+ ): Locator> {
+ return RaceLocator.create(locators);
+ }
+
/**
* Used for nominally typing {@link Locator}.
*/
declare _?: T;
/**
- * Creates a race between multiple locators but ensures that only a single one
- * acts.
- */
- static race>>(
- locators: Locators
- ): Locator> {
- return new RaceLocator(
- locators as Array>>
- );
- }
-
- /**
- * Creates an expectation that is evaluated against located values.
- *
- * If the expectations do not match, then the locator will retry.
- *
* @internal
*/
- expect(predicate: Predicate): Locator {
- return new ExpectedLocator(this, predicate);
- }
+ protected visibility: VisibilityOption = null;
+ /**
+ * @internal
+ */
+ protected timeout = 30_000;
+ #ensureElementIsInTheViewport = true;
+ #waitForEnabled = true;
+ #waitForStableBoundingBox = true;
+
+ /**
+ * @internal
+ */
+ protected operators = {
+ conditions: (
+ conditions: Array>,
+ signal?: AbortSignal
+ ): OperatorFunction, HandleFor> => {
+ return mergeMap((handle: HandleFor) => {
+ return merge(
+ ...conditions.map(condition => {
+ return condition(handle, signal);
+ })
+ ).pipe(defaultIfEmpty(handle));
+ });
+ },
+ retryAndRaceWithSignalAndTimer: (
+ signal?: AbortSignal
+ ): OperatorFunction => {
+ const candidates = [];
+ if (signal) {
+ candidates.push(
+ fromEvent(signal, 'abort').pipe(
+ map(() => {
+ throw signal.reason;
+ })
+ )
+ );
+ }
+ if (this.timeout > 0) {
+ candidates.push(
+ timer(this.timeout).pipe(
+ map(() => {
+ throw new TimeoutError(
+ `Timed out after waiting ${this.timeout}ms`
+ );
+ })
+ )
+ );
+ }
+ return pipe(
+ retry({delay: RETRY_DELAY}),
+ raceWith(...candidates)
+ );
+ },
+ };
override on(
eventName: K,
@@ -162,20 +249,409 @@ export abstract class Locator extends EventEmitter {
return super.off(eventName, handler);
}
- abstract setVisibility(visibility: VisibilityOption): this;
+ setTimeout(timeout: number): this {
+ this.timeout = timeout;
+ return this;
+ }
- abstract setTimeout(timeout: number): this;
+ setVisibility(
+ this: Locator,
+ visibility: VisibilityOption
+ ): Locator {
+ this.visibility = visibility;
+ return this;
+ }
- abstract setEnsureElementIsInTheViewport(value: boolean): this;
+ setWaitForEnabled(
+ this: Locator,
+ value: boolean
+ ): Locator {
+ this.#waitForEnabled = value;
+ return this;
+ }
- abstract setWaitForEnabled(value: boolean): this;
+ setEnsureElementIsInTheViewport(
+ this: Locator,
+ value: boolean
+ ): Locator {
+ this.#ensureElementIsInTheViewport = value;
+ return this;
+ }
- abstract setWaitForStableBoundingBox(value: boolean): this;
+ setWaitForStableBoundingBox(
+ this: Locator,
+ value: boolean
+ ): Locator {
+ this.#waitForStableBoundingBox = value;
+ return this;
+ }
- abstract click(
+ /**
+ * @internal
+ */
+ copyOptions(locator: Locator): this {
+ this.timeout = locator.timeout;
+ this.visibility = locator.visibility;
+ this.#waitForEnabled = locator.#waitForEnabled;
+ this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
+ this.#waitForStableBoundingBox = locator.#waitForStableBoundingBox;
+ return this;
+ }
+
+ /**
+ * If the element has a "disabled" property, wait for the element to be
+ * enabled.
+ */
+ #waitForEnabledIfNeeded = (
+ handle: HandleFor,
+ signal?: AbortSignal
+ ): Observable => {
+ if (!this.#waitForEnabled) {
+ return EMPTY;
+ }
+ return from(
+ handle.frame.page().waitForFunction(
+ element => {
+ if ('disabled' in element && typeof element.disabled === 'boolean') {
+ return !element.disabled;
+ }
+ return true;
+ },
+ {
+ timeout: this.timeout,
+ signal,
+ },
+ handle
+ )
+ ).pipe(ignoreElements());
+ };
+
+ /**
+ * Compares the bounding box of the element for two consecutive animation
+ * frames and waits till they are the same.
+ */
+ #waitForStableBoundingBoxIfNeeded = (
+ handle: HandleFor
+ ): Observable => {
+ if (!this.#waitForStableBoundingBox) {
+ return EMPTY;
+ }
+ return defer(() => {
+ // Note we don't use waitForFunction because that relies on RAF.
+ return from(
+ handle.evaluate(element => {
+ return new Promise<[BoundingBox, BoundingBox]>(resolve => {
+ window.requestAnimationFrame(() => {
+ const rect1 = element.getBoundingClientRect();
+ window.requestAnimationFrame(() => {
+ const rect2 = element.getBoundingClientRect();
+ resolve([
+ {
+ x: rect1.x,
+ y: rect1.y,
+ width: rect1.width,
+ height: rect1.height,
+ },
+ {
+ x: rect2.x,
+ y: rect2.y,
+ width: rect2.width,
+ height: rect2.height,
+ },
+ ]);
+ });
+ });
+ });
+ })
+ );
+ }).pipe(
+ first(([rect1, rect2]) => {
+ return (
+ rect1.x === rect2.x &&
+ rect1.y === rect2.y &&
+ rect1.width === rect2.width &&
+ rect1.height === rect2.height
+ );
+ }),
+ retry({delay: RETRY_DELAY}),
+ ignoreElements()
+ );
+ };
+
+ /**
+ * Checks if the element is in the viewport and auto-scrolls it if it is not.
+ */
+ #ensureElementIsInTheViewportIfNeeded = (
+ handle: HandleFor
+ ): Observable => {
+ if (!this.#ensureElementIsInTheViewport) {
+ return EMPTY;
+ }
+ return from(handle.isIntersectingViewport({threshold: 0})).pipe(
+ filter(isIntersectingViewport => {
+ return !isIntersectingViewport;
+ }),
+ mergeMap(() => {
+ return from(handle.scrollIntoView());
+ }),
+ mergeMap(() => {
+ return defer(() => {
+ return from(handle.isIntersectingViewport({threshold: 0}));
+ }).pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
+ })
+ );
+ };
+
+ #click(
this: Locator,
options?: Readonly
- ): Promise;
+ ): Observable {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ this.#waitForEnabledIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEmittedEvents.Action);
+ }),
+ mergeMap(handle => {
+ return from(handle.click(options)).pipe(
+ catchError((_, caught) => {
+ void handle.dispose().catch(debugError);
+ return caught;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #fill(
+ this: Locator,
+ value: string,
+ options?: Readonly
+ ): Observable {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ this.#waitForEnabledIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEmittedEvents.Action);
+ }),
+ mergeMap(handle => {
+ return from(
+ (handle as unknown as ElementHandle).evaluate(el => {
+ if (el instanceof HTMLSelectElement) {
+ return 'select';
+ }
+ if (el instanceof HTMLInputElement) {
+ if (
+ new Set([
+ 'textarea',
+ 'text',
+ 'url',
+ 'tel',
+ 'search',
+ 'password',
+ 'number',
+ 'email',
+ ]).has(el.type)
+ ) {
+ return 'typeable-input';
+ } else {
+ return 'other-input';
+ }
+ }
+
+ if (el.isContentEditable) {
+ return 'contenteditable';
+ }
+
+ return 'unknown';
+ })
+ )
+ .pipe(
+ mergeMap(inputType => {
+ switch (inputType) {
+ case 'select':
+ return from(handle.select(value).then(noop));
+ case 'contenteditable':
+ case 'typeable-input':
+ return from(
+ (
+ handle as unknown as ElementHandle
+ ).evaluate((input, newValue) => {
+ const currentValue = input.isContentEditable
+ ? input.innerText
+ : input.value;
+
+ // Clear the input if the current value does not match the filled
+ // out value.
+ if (
+ newValue.length <= currentValue.length ||
+ !newValue.startsWith(input.value)
+ ) {
+ if (input.isContentEditable) {
+ input.innerText = '';
+ } else {
+ input.value = '';
+ }
+ return newValue;
+ }
+ const originalValue = input.isContentEditable
+ ? input.innerText
+ : input.value;
+
+ // If the value is partially filled out, only type the rest. Move
+ // cursor to the end of the common prefix.
+ if (input.isContentEditable) {
+ input.innerText = '';
+ input.innerText = originalValue;
+ } else {
+ input.value = '';
+ input.value = originalValue;
+ }
+ return newValue.substring(originalValue.length);
+ }, value)
+ ).pipe(
+ mergeMap(textToType => {
+ return from(handle.type(textToType));
+ })
+ );
+ case 'other-input':
+ return from(handle.focus()).pipe(
+ mergeMap(() => {
+ return from(
+ handle.evaluate((input, value) => {
+ (input as HTMLInputElement).value = value;
+ input.dispatchEvent(
+ new Event('input', {bubbles: true})
+ );
+ input.dispatchEvent(
+ new Event('change', {bubbles: true})
+ );
+ }, value)
+ );
+ })
+ );
+ case 'unknown':
+ throw new Error(`Element cannot be filled out.`);
+ }
+ })
+ )
+ .pipe(
+ catchError((_, caught) => {
+ void handle.dispose().catch(debugError);
+ return caught;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #hover(
+ this: Locator,
+ options?: Readonly
+ ): Observable {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEmittedEvents.Action);
+ }),
+ mergeMap(handle => {
+ return from(handle.hover()).pipe(
+ catchError((_, caught) => {
+ void handle.dispose().catch(debugError);
+ return caught;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ #scroll(
+ this: Locator,
+ options?: Readonly
+ ): Observable {
+ const signal = options?.signal;
+ return this._wait(options).pipe(
+ this.operators.conditions(
+ [
+ this.#ensureElementIsInTheViewportIfNeeded,
+ this.#waitForStableBoundingBoxIfNeeded,
+ ],
+ signal
+ ),
+ tap(() => {
+ return this.emit(LocatorEmittedEvents.Action);
+ }),
+ mergeMap(handle => {
+ return from(
+ handle.evaluate(
+ (el, scrollTop, scrollLeft) => {
+ if (scrollTop !== undefined) {
+ el.scrollTop = scrollTop;
+ }
+ if (scrollLeft !== undefined) {
+ el.scrollLeft = scrollLeft;
+ }
+ },
+ options?.scrollTop,
+ options?.scrollLeft
+ )
+ ).pipe(
+ catchError((_, caught) => {
+ void handle.dispose().catch(debugError);
+ return caught;
+ })
+ );
+ }),
+ this.operators.retryAndRaceWithSignalAndTimer(signal)
+ );
+ }
+
+ /**
+ * @internal
+ */
+ abstract _wait(options?: Readonly): Observable>;
+
+ /**
+ * Creates an expectation that is evaluated against located values.
+ *
+ * If the expectations do not match, then the locator will retry.
+ *
+ * @internal
+ */
+ expect(predicate: Predicate): Locator {
+ return new ExpectedLocator(this, predicate);
+ }
+
+ click(
+ this: Locator,
+ options?: Readonly
+ ): Promise {
+ return firstValueFrom(this.#click(options));
+ }
/**
* Fills out the input identified by the locator using the provided value. The
@@ -183,19 +659,25 @@ export abstract class Locator extends EventEmitter {
* method is chosen based on the type. contenteditable, selector, inputs are
* supported.
*/
- abstract fill(
+ fill(
this: Locator,
value: string,
options?: Readonly
- ): Promise;
+ ): Promise {
+ return firstValueFrom(this.#fill(value, options));
+ }
- abstract hover(
+ hover(
this: Locator,
options?: Readonly
- ): Promise;
+ ): Promise {
+ return firstValueFrom(this.#hover(options));
+ }
- abstract scroll(
+ scroll(
this: Locator,
options?: Readonly
- ): Promise;
+ ): Promise {
+ return firstValueFrom(this.#scroll(options));
+ }
}
diff --git a/packages/puppeteer-core/src/api/locators/NodeLocator.ts b/packages/puppeteer-core/src/api/locators/NodeLocator.ts
index 4534411a1ff..c70ada133fa 100644
--- a/packages/puppeteer-core/src/api/locators/NodeLocator.ts
+++ b/packages/puppeteer-core/src/api/locators/NodeLocator.ts
@@ -1,45 +1,49 @@
-import {TimeoutError} from '../../common/Errors.js';
-import {Awaitable, HandleFor, NodeFor} from '../../common/types.js';
-import {debugError} from '../../common/util.js';
-import {isErrorLike} from '../../util/ErrorLike.js';
-import {BoundingBox, ElementHandle} from '../ElementHandle.js';
-import type {Frame} from '../Frame.js';
-import type {Page} from '../Page.js';
+/**
+ * Copyright 2023 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import {
- ActionOptions,
- LOCATOR_CONTEXTS,
- Locator,
- LocatorClickOptions,
- LocatorEmittedEvents,
- LocatorScrollOptions,
- VisibilityOption,
-} from './locators.js';
+ EMPTY,
+ Observable,
+ defer,
+ filter,
+ first,
+ from,
+ identity,
+ ignoreElements,
+ retry,
+ throwIfEmpty,
+} from '../../../third_party/rxjs/rxjs.js';
+import {HandleFor, NodeFor} from '../../common/types.js';
+import {Frame} from '../Frame.js';
+import {Page} from '../Page.js';
-/**
- * Timeout for individual operations inside the locator. On errors the
- * operation is retried as long as {@link Locator.setTimeout} is not
- * exceeded. This timeout should be generally much lower as locating an
- * element means multiple asynchronious operations.
- */
-const CONDITION_TIMEOUT = 1000;
-const WAIT_FOR_FUNCTION_DELAY = 100;
+import {ActionOptions, Locator, RETRY_DELAY} from './locators.js';
/**
* @internal
*/
-export type ActionCondition = (
+export type Action = (
element: HandleFor,
- signal: AbortSignal
-) => Promise;
+ signal?: AbortSignal
+) => Observable;
/**
* @internal
*/
export class NodeLocator extends Locator {
- /**
- * @internal
- */
static create(
pageOrFrame: Page | Frame,
selector: Selector
@@ -53,447 +57,55 @@ export class NodeLocator extends Locator {
#pageOrFrame: Page | Frame;
#selector: string;
- #visibility: VisibilityOption = 'visible';
- #timeout = 30000;
- #ensureElementIsInTheViewport = true;
- #waitForEnabled = true;
- #waitForStableBoundingBox = true;
- constructor(pageOrFrame: Page | Frame, selector: string) {
+ private constructor(pageOrFrame: Page | Frame, selector: string) {
super();
+
this.#pageOrFrame = pageOrFrame;
this.#selector = selector;
}
- setVisibility(visibility: VisibilityOption): this {
- this.#visibility = visibility;
- return this;
- }
-
- setTimeout(timeout: number): this {
- this.#timeout = timeout;
- return this;
- }
-
- setEnsureElementIsInTheViewport(value: boolean): this {
- this.#ensureElementIsInTheViewport = value;
- return this;
- }
-
- setWaitForEnabled(value: boolean): this {
- this.#waitForEnabled = value;
- return this;
- }
-
- setWaitForStableBoundingBox(value: boolean): this {
- this.#waitForStableBoundingBox = value;
- return this;
- }
-
- /**
- * Retries the `fn` until a truthy result is returned.
- */
- async #waitForFunction(
- fn: (signal: AbortSignal) => Awaitable,
- signal?: AbortSignal,
- timeout = CONDITION_TIMEOUT
- ): Promise {
- let isActive = true;
- let controller: AbortController;
- // If the loop times out, we abort only the last iteration's controller.
- const timeoutId = timeout
- ? setTimeout(() => {
- isActive = false;
- controller?.abort();
- }, timeout)
- : 0;
- // If the user's signal aborts, we abort the last iteration and the loop.
- signal?.addEventListener(
- 'abort',
- () => {
- controller?.abort();
- isActive = false;
- clearTimeout(timeoutId);
- },
- {once: true}
- );
- while (isActive) {
- controller = new AbortController();
- try {
- const result = await fn(controller.signal);
- clearTimeout(timeoutId);
- return result;
- } catch (err) {
- if (isErrorLike(err)) {
- debugError(err);
- // Retry on all timeouts.
- if (err instanceof TimeoutError) {
- continue;
- }
- // Abort error are ignored as they only affect one iteration.
- if (err.name === 'AbortError') {
- continue;
- }
- }
- throw err;
- } finally {
- // We abort any operations that might have been started by `fn`, because
- // the iteration is now over.
- controller.abort();
- }
- await new Promise(resolve => {
- return setTimeout(resolve, WAIT_FOR_FUNCTION_DELAY);
- });
- }
- signal?.throwIfAborted();
- throw new TimeoutError(
- `waitForFunction timed out. The timeout is ${timeout}ms.`
- );
- }
-
- /**
- * Checks if the element is in the viewport and auto-scrolls it if it is not.
- */
- #ensureElementIsInTheViewportIfNeeded = async (
- element: HandleFor,
- signal?: AbortSignal
- ): Promise => {
- if (!this.#ensureElementIsInTheViewport) {
- return;
- }
- // Side-effect: this also checks if it is connected.
- const isIntersectingViewport = await element.isIntersectingViewport({
- threshold: 0,
- });
- signal?.throwIfAborted();
- if (!isIntersectingViewport) {
- await element.scrollIntoView();
- signal?.throwIfAborted();
- await this.#waitForFunction(async () => {
- return await element.isIntersectingViewport({
- threshold: 0,
- });
- }, signal);
- signal?.throwIfAborted();
- }
- };
-
/**
* Waits for the element to become visible or hidden. visibility === 'visible'
* means that the element has a computed style, the visibility property other
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
* 'hidden' means the opposite of that.
*/
- #waitForVisibilityIfNeeded = async (
- element: HandleFor,
- signal?: AbortSignal
- ): Promise => {
- if (this.#visibility === null) {
- return;
+ #waitForVisibilityIfNeeded = (handle: HandleFor): Observable => {
+ if (!this.visibility) {
+ return EMPTY;
}
- if (this.#visibility === 'hidden') {
- await this.#waitForFunction(async () => {
- return element.isHidden();
- }, signal);
- }
- await this.#waitForFunction(async () => {
- return element.isVisible();
- }, signal);
- };
- /**
- * If the element has a "disabled" property, wait for the element to be
- * enabled.
- */
- #waitForEnabledIfNeeded = async (
- element: HandleFor,
- signal?: AbortSignal
- ): Promise => {
- if (!this.#waitForEnabled) {
- return;
- }
- await this.#pageOrFrame.waitForFunction(
- el => {
- if ('disabled' in el && typeof el.disabled === 'boolean') {
- return !el.disabled;
- }
- return true;
- },
- {
- timeout: CONDITION_TIMEOUT,
- signal,
- },
- element
- );
- };
-
- /**
- * Compares the bounding box of the element for two consecutive animation
- * frames and waits till they are the same.
- */
- #waitForStableBoundingBoxIfNeeded = async (
- element: HandleFor,
- signal?: AbortSignal
- ): Promise => {
- if (!this.#waitForStableBoundingBox) {
- return;
- }
- function getClientRect() {
- return element.evaluate(el => {
- return new Promise<[BoundingBox, BoundingBox]>(resolve => {
- window.requestAnimationFrame(() => {
- const rect1 = el.getBoundingClientRect();
- window.requestAnimationFrame(() => {
- const rect2 = el.getBoundingClientRect();
- resolve([
- {
- x: rect1.x,
- y: rect1.y,
- width: rect1.width,
- height: rect1.height,
- },
- {
- x: rect2.x,
- y: rect2.y,
- width: rect2.width,
- height: rect2.height,
- },
- ]);
- });
+ return (() => {
+ switch (this.visibility) {
+ case 'hidden':
+ return defer(() => {
+ return from(handle.isHidden());
});
- });
- });
- }
- await this.#waitForFunction(async () => {
- const [rect1, rect2] = await getClientRect();
- return (
- rect1.x === rect2.x &&
- rect1.y === rect2.y &&
- rect1.width === rect2.width &&
- rect1.height === rect2.height
- );
- }, signal);
+ case 'visible':
+ return defer(() => {
+ return from(handle.isVisible());
+ });
+ }
+ })().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
};
- #run(
- action: (el: HandleFor) => Promise,
- signal?: AbortSignal,
- conditions: Array> = []
- ): Promise {
- const globalConditions = [
- ...(LOCATOR_CONTEXTS.get(this)?.conditions?.values() ?? []),
- ] as Array>;
- const allConditions = conditions.concat(globalConditions);
- return this.#waitForFunction(
- async signal => {
- // 1. Select the element without visibility checks.
- const element = (await this.#pageOrFrame.waitForSelector(
- this.#selector,
- {
- visible: false,
- timeout: this.#timeout,
- signal,
- }
- )) as HandleFor | null;
- // Retry if no element is found.
- if (!element) {
- throw new Error('No element found');
- }
- signal?.throwIfAborted();
- // 2. Perform action specific checks.
- await Promise.all(
- allConditions.map(check => {
- return check(element, signal);
- })
- );
- signal?.throwIfAborted();
- // 3. Perform the action
- this.emit(LocatorEmittedEvents.Action);
- try {
- return await action(element);
- } catch (error) {
- void element.dispose().catch(debugError);
- throw error;
- }
- },
- signal,
- this.#timeout
- );
- }
-
- async click(
- this: NodeLocator,
- options?: Readonly
- ): Promise {
- await this.#run(
- async element => {
- await element.click(options);
- void element.dispose().catch(debugError);
- },
- options?.signal,
- [
- this.#ensureElementIsInTheViewportIfNeeded,
- this.#waitForVisibilityIfNeeded,
- this.#waitForEnabledIfNeeded,
- this.#waitForStableBoundingBoxIfNeeded,
- ]
- );
- }
-
- /**
- * Fills out the input identified by the locator using the provided value. The
- * type of the input is determined at runtime and the appropriate fill-out
- * method is chosen based on the type. contenteditable, selector, inputs are
- * supported.
- */
- async fill(
- this: NodeLocator,
- value: string,
- options?: Readonly
- ): Promise {
- await this.#run(
- async element => {
- const input = element as unknown as ElementHandle;
- const inputType = await input.evaluate(el => {
- if (el instanceof HTMLSelectElement) {
- return 'select';
- }
- if (el instanceof HTMLInputElement) {
- if (
- new Set([
- 'textarea',
- 'text',
- 'url',
- 'tel',
- 'search',
- 'password',
- 'number',
- 'email',
- ]).has(el.type)
- ) {
- return 'typeable-input';
- } else {
- return 'other-input';
- }
- }
-
- if (el.isContentEditable) {
- return 'contenteditable';
- }
-
- return 'unknown';
- });
-
- switch (inputType) {
- case 'select':
- await input.select(value);
- break;
- case 'contenteditable':
- case 'typeable-input':
- const textToType = await (
- input as ElementHandle
- ).evaluate((input, newValue) => {
- const currentValue = input.isContentEditable
- ? input.innerText
- : input.value;
-
- // Clear the input if the current value does not match the filled
- // out value.
- if (
- newValue.length <= currentValue.length ||
- !newValue.startsWith(input.value)
- ) {
- if (input.isContentEditable) {
- input.innerText = '';
- } else {
- input.value = '';
- }
- return newValue;
- }
- const originalValue = input.isContentEditable
- ? input.innerText
- : input.value;
-
- // If the value is partially filled out, only type the rest. Move
- // cursor to the end of the common prefix.
- if (input.isContentEditable) {
- input.innerText = '';
- input.innerText = originalValue;
- } else {
- input.value = '';
- input.value = originalValue;
- }
- return newValue.substring(originalValue.length);
- }, value);
- await input.type(textToType);
- break;
- case 'other-input':
- await input.focus();
- await input.evaluate((input, value) => {
- (input as HTMLInputElement).value = value;
- input.dispatchEvent(new Event('input', {bubbles: true}));
- input.dispatchEvent(new Event('change', {bubbles: true}));
- }, value);
- break;
- case 'unknown':
- throw new Error(`Element cannot be filled out.`);
- }
- void element.dispose().catch(debugError);
- },
- options?.signal,
- [
- this.#ensureElementIsInTheViewportIfNeeded,
- this.#waitForVisibilityIfNeeded,
- this.#waitForEnabledIfNeeded,
- this.#waitForStableBoundingBoxIfNeeded,
- ]
- );
- }
-
- async hover(
- this: NodeLocator,
- options?: Readonly
- ): Promise {
- await this.#run(
- async element => {
- await element.hover();
- void element.dispose().catch(debugError);
- },
- options?.signal,
- [
- this.#ensureElementIsInTheViewportIfNeeded,
- this.#waitForVisibilityIfNeeded,
- this.#waitForStableBoundingBoxIfNeeded,
- ]
- );
- }
-
- async scroll(
- this: NodeLocator,
- options?: Readonly
- ): Promise {
- await this.#run(
- async element => {
- await element.evaluate(
- (el, scrollTop, scrollLeft) => {
- if (scrollTop !== undefined) {
- el.scrollTop = scrollTop;
- }
- if (scrollLeft !== undefined) {
- el.scrollLeft = scrollLeft;
- }
- },
- options?.scrollTop,
- options?.scrollLeft
- );
- void element.dispose().catch(debugError);
- },
- options?.signal,
- [
- this.#ensureElementIsInTheViewportIfNeeded,
- this.#waitForVisibilityIfNeeded,
- this.#waitForStableBoundingBoxIfNeeded,
- ]
+ _wait(options?: Readonly): Observable> {
+ const signal = options?.signal;
+ return defer(() => {
+ return from(
+ this.#pageOrFrame.waitForSelector(this.#selector, {
+ visible: false,
+ timeout: this.timeout,
+ signal,
+ }) as Promise | null>
+ );
+ }).pipe(
+ filter((value): value is NonNullable => {
+ return value !== null;
+ }),
+ throwIfEmpty(),
+ this.operators.conditions([this.#waitForVisibilityIfNeeded], signal)
);
}
}
diff --git a/packages/puppeteer-core/src/api/locators/RaceLocator.ts b/packages/puppeteer-core/src/api/locators/RaceLocator.ts
index 6fd5aeca51c..e5890d42f0f 100644
--- a/packages/puppeteer-core/src/api/locators/RaceLocator.ts
+++ b/packages/puppeteer-core/src/api/locators/RaceLocator.ts
@@ -1,174 +1,63 @@
-import {isErrorLike} from '../../util/ErrorLike.js';
+/**
+ * Copyright 2023 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
-import {
- Locator,
- VisibilityOption,
- LocatorEmittedEvents,
- LocatorClickOptions,
- ActionOptions,
- LocatorScrollOptions,
-} from './locators.js';
+import {Observable, race} from '../../../third_party/rxjs/rxjs.js';
+import {HandleFor} from '../../puppeteer-core.js';
+
+import {ActionOptions, Locator} from './locators.js';
/**
* @public
*/
-export type UnionLocatorOf = T extends Array> ? S : never;
+export type AwaitedLocator = T extends Locator ? S : never;
+
+function checkLocatorArray(
+ locators: T
+): ReadonlyArray>> {
+ for (const locator of locators) {
+ if (!(locator instanceof Locator)) {
+ throw new Error('Unknown locator for race candidate');
+ }
+ }
+ return locators as ReadonlyArray>>;
+}
/**
* @internal
*/
export class RaceLocator extends Locator {
- #locators: Array>;
+ static create(
+ locators: T
+ ): Locator> {
+ const array = checkLocatorArray(locators);
+ return new RaceLocator(array);
+ }
- constructor(locators: Array>) {
+ #locators: ReadonlyArray>;
+
+ constructor(locators: ReadonlyArray>) {
super();
this.#locators = locators;
}
- override setVisibility(visibility: VisibilityOption): this {
- for (const locator of this.#locators) {
- locator.setVisibility(visibility);
- }
- return this;
- }
-
- override setTimeout(timeout: number): this {
- for (const locator of this.#locators) {
- locator.setTimeout(timeout);
- }
- return this;
- }
-
- override setEnsureElementIsInTheViewport(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setEnsureElementIsInTheViewport(value);
- }
- return this;
- }
-
- override setWaitForEnabled(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setWaitForEnabled(value);
- }
- return this;
- }
-
- override setWaitForStableBoundingBox(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setWaitForStableBoundingBox(value);
- }
- return this;
- }
-
- async #run(
- action: (locator: Locator, signal: AbortSignal) => Promise,
- signal?: AbortSignal
- ) {
- const abortControllers = new WeakMap, AbortController>();
-
- // Abort all locators if the user-provided signal aborts.
- signal?.addEventListener('abort', () => {
- for (const locator of this.#locators) {
- abortControllers.get(locator)?.abort();
- }
- });
-
- const handleLocatorAction = (locator: Locator): (() => void) => {
- return () => {
- // When one locator is ready to act, we will abort other locators.
- for (const other of this.#locators) {
- if (other !== locator) {
- abortControllers.get(other)?.abort();
- }
- }
- this.emit(LocatorEmittedEvents.Action);
- };
- };
-
- const createAbortController = (locator: Locator): AbortController => {
- const abortController = new AbortController();
- abortControllers.set(locator, abortController);
- return abortController;
- };
-
- const results = await Promise.allSettled(
- this.#locators.map(locator => {
- return action(
- locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)),
- createAbortController(locator).signal
- );
+ override _wait(options?: Readonly): Observable> {
+ return race(
+ ...this.#locators.map(locator => {
+ return locator._wait(options);
})
);
-
- signal?.throwIfAborted();
-
- const rejected = results.filter(
- (result): result is PromiseRejectedResult => {
- return result.status === 'rejected';
- }
- );
-
- // If some locators are fulfilled, do not throw.
- if (rejected.length !== results.length) {
- return;
- }
-
- for (const result of rejected) {
- const reason = result.reason;
- // AbortError is be an expected result of a race.
- if (isErrorLike(reason) && reason.name === 'AbortError') {
- continue;
- }
- throw reason;
- }
- }
-
- async click(
- this: RaceLocator,
- options?: Readonly
- ): Promise