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.
This commit is contained in:
parent
e8e7bf1d3c
commit
ede43ca2d3
@ -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 <code>puppeteer.launch</code> 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) | |
|
||||
|
13
docs/api/puppeteer.awaitedlocator.md
Normal file
13
docs/api/puppeteer.awaitedlocator.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
sidebar_label: AwaitedLocator
|
||||
---
|
||||
|
||||
# AwaitedLocator type
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;
|
||||
```
|
||||
|
||||
**References:** [Locator](./puppeteer.locator.md)
|
@ -8,7 +8,7 @@ sidebar_label: Locator.click
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract click<ElementType extends Element>(
|
||||
click<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void>;
|
||||
|
@ -10,7 +10,7 @@ Fills out the input identified by the locator using the provided value. The type
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract fill<ElementType extends Element>(
|
||||
fill<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.hover
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract hover<ElementType extends Element>(
|
||||
hover<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
|
@ -22,18 +22,18 @@ export declare abstract class Locator<T> 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) | <code>static</code> | 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) | <code>static</code> | 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) | | |
|
||||
|
@ -10,9 +10,9 @@ Creates a race between multiple locators but ensures that only a single one acts
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
static race<Locators extends Array<Locator<unknown>>>(
|
||||
static race<Locators extends readonly unknown[] | []>(
|
||||
locators: Locators
|
||||
): Locator<UnionLocatorOf<Locators>>;
|
||||
): Locator<AwaitedLocator<Locators[number]>>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -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\]>>
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.scroll
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract scroll<ElementType extends Element>(
|
||||
scroll<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void>;
|
||||
|
@ -8,16 +8,20 @@ sidebar_label: Locator.setEnsureElementIsInTheViewport
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract setEnsureElementIsInTheViewport(value: boolean): this;
|
||||
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ----------- |
|
||||
| value | boolean | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---------------------------------------------------- | ----------- |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| value | boolean | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
this
|
||||
[Locator](./puppeteer.locator.md)<ElementType>
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract setTimeout(timeout: number): this;
|
||||
setTimeout(timeout: number): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,10 @@ sidebar_label: Locator.setVisibility
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract setVisibility(visibility: VisibilityOption): this;
|
||||
setVisibility<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
visibility: VisibilityOption
|
||||
): Locator<NodeType>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -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>
|
||||
|
@ -8,16 +8,20 @@ sidebar_label: Locator.setWaitForEnabled
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract setWaitForEnabled(value: boolean): this;
|
||||
setWaitForEnabled<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
value: boolean
|
||||
): Locator<NodeType>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ----------- |
|
||||
| value | boolean | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------------------- | ----------- |
|
||||
| this | [Locator](./puppeteer.locator.md)<NodeType> | |
|
||||
| value | boolean | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
this
|
||||
[Locator](./puppeteer.locator.md)<NodeType>
|
||||
|
@ -8,16 +8,20 @@ sidebar_label: Locator.setWaitForStableBoundingBox
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract setWaitForStableBoundingBox(value: boolean): this;
|
||||
setWaitForStableBoundingBox<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ----------- |
|
||||
| value | boolean | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---------------------------------------------------- | ----------- |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| value | boolean | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
this
|
||||
[Locator](./puppeteer.locator.md)<ElementType>
|
||||
|
@ -1,13 +0,0 @@
|
||||
---
|
||||
sidebar_label: UnionLocatorOf
|
||||
---
|
||||
|
||||
# UnionLocatorOf type
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export type UnionLocatorOf<T> = T extends Array<Locator<infer S>> ? S : never;
|
||||
```
|
||||
|
||||
**References:** [Locator](./puppeteer.locator.md)
|
409
package-lock.json
generated
409
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -152,6 +152,7 @@
|
||||
"@puppeteer/browsers": "1.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rxjs": "7.8.1",
|
||||
"mitt": "3.0.0",
|
||||
"parsel-js": "1.1.0"
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<Locators extends Array<Locator<unknown>>>(
|
||||
locatorRace<Locators extends readonly unknown[] | []>(
|
||||
locators: Locators
|
||||
): Locator<UnionLocatorOf<Locators>> {
|
||||
return Locator.race(locators as Array<Locator<UnionLocatorOf<Locators>>>);
|
||||
): Locator<AwaitedLocator<Locators[number]>> {
|
||||
return Locator.race(locators);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<From, To extends From> extends Locator<To> {
|
||||
|
||||
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<FromNode extends Node, ToNode extends FromNode>(
|
||||
this: ExpectedLocator<FromNode, ToNode>,
|
||||
visibility: VisibilityOption
|
||||
): Locator<ToNode> {
|
||||
super.setVisibility(visibility);
|
||||
this.#base.setVisibility(visibility);
|
||||
return this;
|
||||
}
|
||||
override setWaitForEnabled(value: boolean): this {
|
||||
|
||||
override setWaitForEnabled<FromNode extends Node, ToNode extends FromNode>(
|
||||
this: ExpectedLocator<FromNode, ToNode>,
|
||||
value: boolean
|
||||
): Locator<ToNode> {
|
||||
super.setWaitForEnabled(value);
|
||||
this.#base.setWaitForEnabled(value);
|
||||
return this;
|
||||
}
|
||||
override setWaitForStableBoundingBox(value: boolean): this {
|
||||
|
||||
override setEnsureElementIsInTheViewport<
|
||||
FromElement extends Element,
|
||||
ToElement extends FromElement,
|
||||
>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
value: boolean
|
||||
): Locator<ToElement> {
|
||||
super.setEnsureElementIsInTheViewport(value);
|
||||
this.#base.setEnsureElementIsInTheViewport(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
override setWaitForStableBoundingBox<
|
||||
FromElement extends Element,
|
||||
ToElement extends FromElement,
|
||||
>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
value: boolean
|
||||
): Locator<ToElement> {
|
||||
super.setWaitForStableBoundingBox(value);
|
||||
this.#base.setWaitForStableBoundingBox(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
#condition: ActionCondition<From> = async (handle, signal) => {
|
||||
// TODO(jrandolf): We should remove this once JSHandle has waitForFunction.
|
||||
await (handle as ElementHandle<Node>).frame.waitForFunction(
|
||||
this.#predicate,
|
||||
{signal},
|
||||
handle
|
||||
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
|
||||
return this.#base._wait(options).pipe(
|
||||
mergeMap(handle => {
|
||||
return from(
|
||||
(handle as ElementHandle<Node>).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<To>;
|
||||
})
|
||||
);
|
||||
}),
|
||||
throwIfEmpty()
|
||||
);
|
||||
};
|
||||
|
||||
#insertFilterCondition<
|
||||
FromElement extends Node,
|
||||
ToElement extends FromElement,
|
||||
>(this: ExpectedLocator<FromElement, ToElement>): void {
|
||||
const context = (LOCATOR_CONTEXTS.get(this.#base) ??
|
||||
{}) as LocatorContext<FromElement>;
|
||||
context.conditions ??= new Set();
|
||||
context.conditions.add(this.#condition);
|
||||
LOCATOR_CONTEXTS.set(this.#base, context);
|
||||
}
|
||||
|
||||
override click<FromElement extends Element, ToElement extends FromElement>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
this.#insertFilterCondition();
|
||||
return this.#base.click(options);
|
||||
}
|
||||
override fill<FromElement extends Element, ToElement extends FromElement>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
this.#insertFilterCondition();
|
||||
return this.#base.fill(value, options);
|
||||
}
|
||||
override hover<FromElement extends Element, ToElement extends FromElement>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
this.#insertFilterCondition();
|
||||
return this.#base.hover(options);
|
||||
}
|
||||
override scroll<FromElement extends Element, ToElement extends FromElement>(
|
||||
this: ExpectedLocator<FromElement, ToElement>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void> {
|
||||
this.#insertFilterCondition();
|
||||
return this.#base.scroll(options);
|
||||
}
|
||||
}
|
||||
|
@ -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<T> {
|
||||
conditions?: Set<ActionCondition<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LOCATOR_CONTEXTS = new WeakMap<
|
||||
Locator<unknown>,
|
||||
LocatorContext<never>
|
||||
>();
|
||||
export const RETRY_DELAY = 100;
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -113,33 +152,81 @@ export interface LocatorEventObject {
|
||||
* @public
|
||||
*/
|
||||
export abstract class Locator<T> extends EventEmitter {
|
||||
/**
|
||||
* Creates a race between multiple locators but ensures that only a single one
|
||||
* acts.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static race<Locators extends readonly unknown[] | []>(
|
||||
locators: Locators
|
||||
): Locator<AwaitedLocator<Locators[number]>> {
|
||||
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 extends Array<Locator<unknown>>>(
|
||||
locators: Locators
|
||||
): Locator<UnionLocatorOf<Locators>> {
|
||||
return new RaceLocator(
|
||||
locators as Array<Locator<UnionLocatorOf<Locators>>>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expectation that is evaluated against located values.
|
||||
*
|
||||
* If the expectations do not match, then the locator will retry.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
expect<S extends T>(predicate: Predicate<T, S>): Locator<S> {
|
||||
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<Action<T, never>>,
|
||||
signal?: AbortSignal
|
||||
): OperatorFunction<HandleFor<T>, HandleFor<T>> => {
|
||||
return mergeMap((handle: HandleFor<T>) => {
|
||||
return merge(
|
||||
...conditions.map(condition => {
|
||||
return condition(handle, signal);
|
||||
})
|
||||
).pipe(defaultIfEmpty(handle));
|
||||
});
|
||||
},
|
||||
retryAndRaceWithSignalAndTimer: <T>(
|
||||
signal?: AbortSignal
|
||||
): OperatorFunction<T, T> => {
|
||||
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<T, never[]>(...candidates)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
override on<K extends keyof LocatorEventObject>(
|
||||
eventName: K,
|
||||
@ -162,20 +249,409 @@ export abstract class Locator<T> 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<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
visibility: VisibilityOption
|
||||
): Locator<NodeType> {
|
||||
this.visibility = visibility;
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract setEnsureElementIsInTheViewport(value: boolean): this;
|
||||
setWaitForEnabled<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
value: boolean
|
||||
): Locator<NodeType> {
|
||||
this.#waitForEnabled = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract setWaitForEnabled(value: boolean): this;
|
||||
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType> {
|
||||
this.#ensureElementIsInTheViewport = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract setWaitForStableBoundingBox(value: boolean): this;
|
||||
setWaitForStableBoundingBox<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType> {
|
||||
this.#waitForStableBoundingBox = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract click<ElementType extends Element>(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
copyOptions(locator: Locator<any>): 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 = <ElementType extends Node>(
|
||||
handle: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Observable<never> => {
|
||||
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 = <ElementType extends Element>(
|
||||
handle: HandleFor<ElementType>
|
||||
): Observable<never> => {
|
||||
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 = <ElementType extends Element>(
|
||||
handle: HandleFor<ElementType>
|
||||
): Observable<never> => {
|
||||
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<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void>;
|
||||
): Observable<void> {
|
||||
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<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Observable<void> {
|
||||
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<HTMLElement>).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<HTMLInputElement>
|
||||
).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<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Observable<void> {
|
||||
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<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Observable<void> {
|
||||
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<ActionOptions>): Observable<HandleFor<T>>;
|
||||
|
||||
/**
|
||||
* Creates an expectation that is evaluated against located values.
|
||||
*
|
||||
* If the expectations do not match, then the locator will retry.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
expect<S extends T>(predicate: Predicate<T, S>): Locator<S> {
|
||||
return new ExpectedLocator(this, predicate);
|
||||
}
|
||||
|
||||
click<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
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<T> extends EventEmitter {
|
||||
* method is chosen based on the type. contenteditable, selector, inputs are
|
||||
* supported.
|
||||
*/
|
||||
abstract fill<ElementType extends Element>(
|
||||
fill<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
): Promise<void> {
|
||||
return firstValueFrom(this.#fill(value, options));
|
||||
}
|
||||
|
||||
abstract hover<ElementType extends Element>(
|
||||
hover<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
): Promise<void> {
|
||||
return firstValueFrom(this.#hover(options));
|
||||
}
|
||||
|
||||
abstract scroll<ElementType extends Element>(
|
||||
scroll<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void>;
|
||||
): Promise<void> {
|
||||
return firstValueFrom(this.#scroll(options));
|
||||
}
|
||||
}
|
||||
|
@ -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<T> = (
|
||||
export type Action<T, U> = (
|
||||
element: HandleFor<T>,
|
||||
signal: AbortSignal
|
||||
) => Promise<void>;
|
||||
signal?: AbortSignal
|
||||
) => Observable<U>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class NodeLocator<T extends Node> extends Locator<T> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static create<Selector extends string>(
|
||||
pageOrFrame: Page | Frame,
|
||||
selector: Selector
|
||||
@ -53,447 +57,55 @@ export class NodeLocator<T extends Node> extends Locator<T> {
|
||||
|
||||
#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<T>(
|
||||
fn: (signal: AbortSignal) => Awaitable<T>,
|
||||
signal?: AbortSignal,
|
||||
timeout = CONDITION_TIMEOUT
|
||||
): Promise<T> {
|
||||
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 <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
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 <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (this.#visibility === null) {
|
||||
return;
|
||||
#waitForVisibilityIfNeeded = (handle: HandleFor<T>): Observable<never> => {
|
||||
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 <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
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 <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
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<U>(
|
||||
action: (el: HandleFor<T>) => Promise<U>,
|
||||
signal?: AbortSignal,
|
||||
conditions: Array<ActionCondition<T>> = []
|
||||
): Promise<U> {
|
||||
const globalConditions = [
|
||||
...(LOCATOR_CONTEXTS.get(this)?.conditions?.values() ?? []),
|
||||
] as Array<ActionCondition<T>>;
|
||||
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<T> | 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<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
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<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
await this.#run(
|
||||
async element => {
|
||||
const input = element as unknown as ElementHandle<HTMLElement>;
|
||||
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<HTMLInputElement>
|
||||
).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<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
await this.#run(
|
||||
async element => {
|
||||
await element.hover();
|
||||
void element.dispose().catch(debugError);
|
||||
},
|
||||
options?.signal,
|
||||
[
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async scroll<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void> {
|
||||
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<ActionOptions>): Observable<HandleFor<T>> {
|
||||
const signal = options?.signal;
|
||||
return defer(() => {
|
||||
return from(
|
||||
this.#pageOrFrame.waitForSelector(this.#selector, {
|
||||
visible: false,
|
||||
timeout: this.timeout,
|
||||
signal,
|
||||
}) as Promise<HandleFor<T> | null>
|
||||
);
|
||||
}).pipe(
|
||||
filter((value): value is NonNullable<typeof value> => {
|
||||
return value !== null;
|
||||
}),
|
||||
throwIfEmpty(),
|
||||
this.operators.conditions([this.#waitForVisibilityIfNeeded], signal)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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> = T extends Array<Locator<infer S>> ? S : never;
|
||||
export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;
|
||||
|
||||
function checkLocatorArray<T extends readonly unknown[] | []>(
|
||||
locators: T
|
||||
): ReadonlyArray<Locator<AwaitedLocator<T[number]>>> {
|
||||
for (const locator of locators) {
|
||||
if (!(locator instanceof Locator)) {
|
||||
throw new Error('Unknown locator for race candidate');
|
||||
}
|
||||
}
|
||||
return locators as ReadonlyArray<Locator<AwaitedLocator<T[number]>>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class RaceLocator<T> extends Locator<T> {
|
||||
#locators: Array<Locator<T>>;
|
||||
static create<T extends readonly unknown[]>(
|
||||
locators: T
|
||||
): Locator<AwaitedLocator<T[number]>> {
|
||||
const array = checkLocatorArray(locators);
|
||||
return new RaceLocator(array);
|
||||
}
|
||||
|
||||
constructor(locators: Array<Locator<T>>) {
|
||||
#locators: ReadonlyArray<Locator<T>>;
|
||||
|
||||
constructor(locators: ReadonlyArray<Locator<T>>) {
|
||||
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<T>, signal: AbortSignal) => Promise<void>,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const abortControllers = new WeakMap<Locator<T>, 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<T>): (() => 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<T>): 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<ActionOptions>): Observable<HandleFor<T>> {
|
||||
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<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.click({...options, signal});
|
||||
},
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
async fill<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
value: string,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.fill(value, {...options, signal});
|
||||
},
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
async hover<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.hover({...options, signal});
|
||||
},
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
async scroll<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.scroll({...options, signal});
|
||||
},
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,19 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './Locator.js';
|
||||
export * from './NodeLocator.js';
|
||||
export * from './ExpectedLocator.js';
|
||||
|
41
packages/puppeteer-core/third_party/rxjs/rxjs.ts
vendored
Normal file
41
packages/puppeteer-core/third_party/rxjs/rxjs.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export {
|
||||
catchError,
|
||||
defaultIfEmpty,
|
||||
filter,
|
||||
first,
|
||||
ignoreElements,
|
||||
map,
|
||||
mergeMap,
|
||||
raceWith,
|
||||
retry,
|
||||
tap,
|
||||
throwIfEmpty,
|
||||
firstValueFrom,
|
||||
defer,
|
||||
EMPTY,
|
||||
from,
|
||||
fromEvent,
|
||||
merge,
|
||||
race,
|
||||
timer,
|
||||
OperatorFunction,
|
||||
identity,
|
||||
noop,
|
||||
pipe,
|
||||
Observable,
|
||||
} from 'rxjs';
|
@ -236,7 +236,7 @@ describe('Locator', function () {
|
||||
const result = page.locator('button').click();
|
||||
clock.tick(5100);
|
||||
await expect(result).rejects.toEqual(
|
||||
new TimeoutError('waitForFunction timed out. The timeout is 5000ms.')
|
||||
new TimeoutError('Timed out after waiting 5000ms')
|
||||
);
|
||||
} finally {
|
||||
clock.restore();
|
||||
@ -257,7 +257,7 @@ describe('Locator', function () {
|
||||
const result = page.locator('button').click();
|
||||
clock.tick(5100);
|
||||
await expect(result).rejects.toEqual(
|
||||
new TimeoutError('waitForFunction timed out. The timeout is 5000ms.')
|
||||
new TimeoutError('Timed out after waiting 5000ms')
|
||||
);
|
||||
} finally {
|
||||
clock.restore();
|
||||
@ -513,15 +513,16 @@ describe('Locator', function () {
|
||||
});
|
||||
try {
|
||||
const {page} = await getTestState();
|
||||
page.setDefaultTimeout(5000);
|
||||
await page.setContent(`<button>test</button>`);
|
||||
const result = Locator.race([
|
||||
page.locator('not-found'),
|
||||
page.locator('not-found'),
|
||||
]).click();
|
||||
])
|
||||
.setTimeout(5000)
|
||||
.click();
|
||||
clock.tick(5100);
|
||||
await expect(result).rejects.toEqual(
|
||||
new TimeoutError('waitForFunction timed out. The timeout is 5000ms.')
|
||||
new TimeoutError('Timed out after waiting 5000ms')
|
||||
);
|
||||
} finally {
|
||||
clock.restore();
|
||||
|
Loading…
Reference in New Issue
Block a user