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:
jrandolf 2023-07-25 12:43:07 +02:00 committed by GitHub
parent e8e7bf1d3c
commit ede43ca2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1083 additions and 971 deletions

View File

@ -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) | |

View 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)

View File

@ -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>;

View File

@ -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>

View File

@ -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>;

View File

@ -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) | | |

View File

@ -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)&lt;[UnionLocatorOf](./puppeteer.unionlocatorof.md)&lt;Locators&gt;&gt;
[Locator](./puppeteer.locator.md)&lt;[AwaitedLocator](./puppeteer.awaitedlocator.md)&lt;Locators\[number\]&gt;&gt;

View File

@ -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>;

View File

@ -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)&lt;ElementType&gt; | |
| value | boolean | |
**Returns:**
this
[Locator](./puppeteer.locator.md)&lt;ElementType&gt;

View File

@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
```typescript
class Locator {
abstract setTimeout(timeout: number): this;
setTimeout(timeout: number): this;
}
```

View File

@ -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)&lt;NodeType&gt; | |
| visibility | [VisibilityOption](./puppeteer.visibilityoption.md) | |
**Returns:**
this
[Locator](./puppeteer.locator.md)&lt;NodeType&gt;

View File

@ -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)&lt;NodeType&gt; | |
| value | boolean | |
**Returns:**
this
[Locator](./puppeteer.locator.md)&lt;NodeType&gt;

View File

@ -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)&lt;ElementType&gt; | |
| value | boolean | |
**Returns:**
this
[Locator](./puppeteer.locator.md)&lt;ElementType&gt;

View File

@ -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
View File

@ -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": {

View File

@ -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",

View File

@ -152,6 +152,7 @@
"@puppeteer/browsers": "1.4.6"
},
"devDependencies": {
"rxjs": "7.8.1",
"mitt": "3.0.0",
"parsel-js": "1.1.0"
}

View File

@ -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;

View File

@ -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);
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -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';

View 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';

View File

@ -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();