chore: implement typed Locators, expects, and internal contexts (#10573)
This commit is contained in:
parent
dcc08aa6b8
commit
c14f9b64a7
@ -97,6 +97,7 @@ sidebar_label: API
|
||||
| [LaunchOptions](./puppeteer.launchoptions.md) | Generic launch options that can be passed when launching any browser. |
|
||||
| [LocatorEventObject](./puppeteer.locatoreventobject.md) | |
|
||||
| [LocatorOptions](./puppeteer.locatoroptions.md) | |
|
||||
| [LocatorScrollOptions](./puppeteer.locatorscrolloptions.md) | |
|
||||
| [MediaFeature](./puppeteer.mediafeature.md) | |
|
||||
| [Metrics](./puppeteer.metrics.md) | |
|
||||
| [MouseClickOptions](./puppeteer.mouseclickoptions.md) | |
|
||||
@ -147,7 +148,6 @@ sidebar_label: API
|
||||
|
||||
| Type Alias | Description |
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [ActionCondition](./puppeteer.actioncondition.md) | |
|
||||
| [ActionResult](./puppeteer.actionresult.md) | |
|
||||
| [Awaitable](./puppeteer.awaitable.md) | |
|
||||
| [AwaitableIterable](./puppeteer.awaitableiterable.md) | |
|
||||
@ -167,11 +167,13 @@ sidebar_label: API
|
||||
| [InterceptResolutionStrategy](./puppeteer.interceptresolutionstrategy.md) | |
|
||||
| [KeyInput](./puppeteer.keyinput.md) | All the valid keys that can be passed to functions that take user input, such as [keyboard.press](./puppeteer.keyboard.press.md) |
|
||||
| [KeyPressOptions](./puppeteer.keypressoptions.md) | |
|
||||
| [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | |
|
||||
| [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | |
|
||||
| [MouseButton](./puppeteer.mousebutton.md) | |
|
||||
| [NodeFor](./puppeteer.nodefor.md) | |
|
||||
| [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. |
|
||||
| [Permission](./puppeteer.permission.md) | |
|
||||
| [Predicate](./puppeteer.predicate.md) | |
|
||||
| [Product](./puppeteer.product.md) | Supported products. |
|
||||
| [ProtocolLifeCycleEvent](./puppeteer.protocollifecycleevent.md) | |
|
||||
| [PuppeteerLifeCycleEvent](./puppeteer.puppeteerlifecycleevent.md) | |
|
||||
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_label: ActionCondition
|
||||
---
|
||||
|
||||
# ActionCondition type
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export type ActionCondition = (
|
||||
element: ElementHandle,
|
||||
signal: AbortSignal
|
||||
) => Promise<void>;
|
||||
```
|
||||
|
||||
**References:** [ElementHandle](./puppeteer.elementhandle.md)
|
@ -12,7 +12,6 @@ export interface ActionOptions
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description | Default |
|
||||
| ---------- | --------------------- | ----------------------------------------------------- | ----------- | ------- |
|
||||
| conditions | | [ActionCondition](./puppeteer.actioncondition.md)\[\] | | |
|
||||
| signal | <code>optional</code> | AbortSignal | | |
|
||||
| Property | Modifiers | Type | Description | Default |
|
||||
| -------- | --------------------- | ----------- | ----------- | ------- |
|
||||
| signal | <code>optional</code> | AbortSignal | | |
|
||||
|
@ -10,19 +10,21 @@ Creates a locator for the provided `selector`. See [Locator](./puppeteer.locator
|
||||
|
||||
```typescript
|
||||
class Frame {
|
||||
locator(selector: string): Locator;
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
): Locator<NodeFor<Selector>>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------ | ----------- |
|
||||
| selector | string | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | -------- | ----------- |
|
||||
| selector | Selector | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
[Locator](./puppeteer.locator.md)
|
||||
[Locator](./puppeteer.locator.md)<[NodeFor](./puppeteer.nodefor.md)<Selector>>
|
||||
|
||||
## Remarks
|
||||
|
||||
|
@ -8,19 +8,19 @@ sidebar_label: Locator.click
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
abstract click<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------ | --------------------------------------------------------------------------- | ------------ |
|
||||
| clickOptions | [ClickOptions](./puppeteer.clickoptions.md) & { signal?: AbortSignal; } | _(Optional)_ |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------------------------------------------- | ------------ |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| options | Readonly<[LocatorClickOptions](./puppeteer.locatorclickoptions.md)> | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
@ -10,21 +10,21 @@ Fills out the input identified by the locator using the provided value. The type
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract fill(
|
||||
abstract fill<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: string,
|
||||
fillOptions?: {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ----------- | ------------------------- | ------------ |
|
||||
| value | string | |
|
||||
| fillOptions | { signal?: AbortSignal; } | _(Optional)_ |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------------------------------- | ------------ |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| value | string | |
|
||||
| options | Readonly<[ActionOptions](./puppeteer.actionoptions.md)> | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
@ -8,15 +8,19 @@ sidebar_label: Locator.hover
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract hover(hoverOptions?: {signal?: AbortSignal}): Promise<void>;
|
||||
abstract hover<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------ | ------------------------- | ------------ |
|
||||
| hoverOptions | { signal?: AbortSignal; } | _(Optional)_ |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------------------------------- | ------------ |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| options | Readonly<[ActionOptions](./puppeteer.actionoptions.md)> | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
@ -9,23 +9,29 @@ Locators describe a strategy of locating elements and performing an action on th
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export declare abstract class Locator extends EventEmitter
|
||||
export declare abstract class Locator<T> extends EventEmitter
|
||||
```
|
||||
|
||||
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| -------- | --------------------- | ---- | ------------------------------------------------------------ |
|
||||
| \_ | <code>optional</code> | T | Used for nominally typing [Locator](./puppeteer.locator.md). |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| ------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [click(clickOptions)](./puppeteer.locator.click.md) | | |
|
||||
| [fill(value, fillOptions)](./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(hoverOptions)](./puppeteer.locator.hover.md) | | |
|
||||
| [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(scrollOptions)](./puppeteer.locator.scroll.md) | | |
|
||||
| [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) | | |
|
||||
|
@ -10,16 +10,18 @@ Creates a race between multiple locators but ensures that only a single one acts
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
static race(locators: Locator[]): Locator;
|
||||
static race<Locators extends Array<Locator<unknown>>>(
|
||||
locators: Locators
|
||||
): Locator<UnionLocatorOf<Locators>>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------- | ----------- |
|
||||
| locators | [Locator](./puppeteer.locator.md)\[\] | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | -------- | ----------- |
|
||||
| locators | Locators | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
[Locator](./puppeteer.locator.md)
|
||||
[Locator](./puppeteer.locator.md)<UnionLocatorOf<Locators>>
|
||||
|
@ -8,19 +8,19 @@ sidebar_label: Locator.scroll
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
abstract scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void>;
|
||||
abstract scroll<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------- | ------------------------------------------------------------------ | ------------ |
|
||||
| scrollOptions | { scrollTop?: number; scrollLeft?: number; signal?: AbortSignal; } | _(Optional)_ |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | --------------------------------------------------------------------------- | ------------ |
|
||||
| this | [Locator](./puppeteer.locator.md)<ElementType> | |
|
||||
| options | Readonly<[LocatorScrollOptions](./puppeteer.locatorscrolloptions.md)> | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
13
docs/api/puppeteer.locatorclickoptions.md
Normal file
13
docs/api/puppeteer.locatorclickoptions.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
sidebar_label: LocatorClickOptions
|
||||
---
|
||||
|
||||
# LocatorClickOptions type
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export type LocatorClickOptions = ClickOptions & ActionOptions;
|
||||
```
|
||||
|
||||
**References:** [ClickOptions](./puppeteer.clickoptions.md), [ActionOptions](./puppeteer.actionoptions.md)
|
20
docs/api/puppeteer.locatorscrolloptions.md
Normal file
20
docs/api/puppeteer.locatorscrolloptions.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
sidebar_label: LocatorScrollOptions
|
||||
---
|
||||
|
||||
# LocatorScrollOptions interface
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export interface LocatorScrollOptions extends ActionOptions
|
||||
```
|
||||
|
||||
**Extends:** [ActionOptions](./puppeteer.actionoptions.md)
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description | Default |
|
||||
| ---------- | --------------------- | ------ | ----------- | ------- |
|
||||
| scrollLeft | <code>optional</code> | number | | |
|
||||
| scrollTop | <code>optional</code> | number | | |
|
@ -10,19 +10,21 @@ Creates a locator for the provided `selector`. See [Locator](./puppeteer.locator
|
||||
|
||||
```typescript
|
||||
class Page {
|
||||
locator(selector: string): Locator;
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
): Locator<NodeFor<Selector>>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------ | ----------- |
|
||||
| selector | string | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | -------- | ----------- |
|
||||
| selector | Selector | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
[Locator](./puppeteer.locator.md)
|
||||
[Locator](./puppeteer.locator.md)<[NodeFor](./puppeteer.nodefor.md)<Selector>>
|
||||
|
||||
## Remarks
|
||||
|
||||
|
15
docs/api/puppeteer.predicate.md
Normal file
15
docs/api/puppeteer.predicate.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
sidebar_label: Predicate
|
||||
---
|
||||
|
||||
# Predicate type
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export type Predicate<From, To extends From = From> =
|
||||
| ((value: From) => value is To)
|
||||
| ((value: From) => Awaitable<boolean>);
|
||||
```
|
||||
|
||||
**References:** [Awaitable](./puppeteer.awaitable.md)
|
@ -424,7 +424,9 @@ export class Frame {
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
*/
|
||||
locator(selector: string): Locator {
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
): Locator<NodeFor<Selector>> {
|
||||
return Locator.create(this, selector);
|
||||
}
|
||||
|
||||
|
@ -16,13 +16,20 @@
|
||||
|
||||
import {TimeoutError} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {Awaitable, HandleFor, NodeFor} from '../common/types.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {ElementHandle, BoundingBox, ClickOptions} from './ElementHandle.js';
|
||||
import {BoundingBox, ClickOptions, ElementHandle} from './ElementHandle.js';
|
||||
import type {Frame} from './Frame.js';
|
||||
import type {Page} from './Page.js';
|
||||
|
||||
interface LocatorContext<T> {
|
||||
conditions?: Set<ActionCondition<T>>;
|
||||
}
|
||||
|
||||
const LOCATOR_CONTEXTS = new WeakMap<Locator<unknown>, LocatorContext<never>>();
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -74,19 +81,38 @@ const CONDITION_TIMEOUT = 1_000;
|
||||
const WAIT_FOR_FUNCTION_DELAY = 100;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export type ActionCondition = (
|
||||
element: ElementHandle,
|
||||
export type ActionCondition<T> = (
|
||||
element: HandleFor<T>,
|
||||
signal: AbortSignal
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Predicate<From, To extends From = From> =
|
||||
| ((value: From) => value is To)
|
||||
| ((value: From) => Awaitable<boolean>);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ActionOptions {
|
||||
signal?: AbortSignal;
|
||||
conditions: ActionCondition[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type LocatorClickOptions = ClickOptions & ActionOptions;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface LocatorScrollOptions extends ActionOptions {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +134,8 @@ export interface LocatorEventObject {
|
||||
[LocatorEmittedEvents.Action]: never;
|
||||
}
|
||||
|
||||
type UnionLocatorOf<T> = T extends Array<Locator<infer S>> ? S : never;
|
||||
|
||||
/**
|
||||
* Locators describe a strategy of locating elements and performing an action on
|
||||
* them. If the action fails because the element is not ready for the action,
|
||||
@ -116,12 +144,20 @@ export interface LocatorEventObject {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export abstract class Locator extends EventEmitter {
|
||||
export abstract class Locator<T> extends EventEmitter {
|
||||
/**
|
||||
* Used for nominally typing {@link Locator}.
|
||||
*/
|
||||
declare _?: T;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static create(pageOrFrame: Page | Frame, selector: string): Locator {
|
||||
return new LocatorImpl(pageOrFrame, selector).setTimeout(
|
||||
static create<Selector extends string>(
|
||||
pageOrFrame: Page | Frame,
|
||||
selector: Selector
|
||||
): Locator<NodeFor<Selector>> {
|
||||
return new NodeLocator<NodeFor<Selector>>(pageOrFrame, selector).setTimeout(
|
||||
'getDefaultTimeout' in pageOrFrame
|
||||
? pageOrFrame.getDefaultTimeout()
|
||||
: pageOrFrame.page().getDefaultTimeout()
|
||||
@ -132,8 +168,23 @@ export abstract class Locator extends EventEmitter {
|
||||
* Creates a race between multiple locators but ensures that only a single one
|
||||
* acts.
|
||||
*/
|
||||
static race(locators: Locator[]): Locator {
|
||||
return new RaceLocatorImpl(locators);
|
||||
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);
|
||||
}
|
||||
|
||||
override on<K extends keyof LocatorEventObject>(
|
||||
@ -167,10 +218,9 @@ export abstract class Locator extends EventEmitter {
|
||||
|
||||
abstract setWaitForStableBoundingBox(value: boolean): this;
|
||||
|
||||
abstract click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
abstract click<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
@ -179,24 +229,27 @@ export abstract class Locator extends EventEmitter {
|
||||
* method is chosen based on the type. contenteditable, selector, inputs are
|
||||
* supported.
|
||||
*/
|
||||
abstract fill(
|
||||
abstract fill<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: string,
|
||||
fillOptions?: {signal?: AbortSignal}
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
|
||||
abstract hover(hoverOptions?: {signal?: AbortSignal}): Promise<void>;
|
||||
abstract hover<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void>;
|
||||
|
||||
abstract scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void>;
|
||||
abstract scroll<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class LocatorImpl extends Locator {
|
||||
export class NodeLocator<T extends Node> extends Locator<T> {
|
||||
#pageOrFrame: Page | Frame;
|
||||
#selector: string;
|
||||
#visibility: VisibilityOption = 'visible';
|
||||
@ -302,8 +355,8 @@ export class LocatorImpl extends Locator {
|
||||
/**
|
||||
* Checks if the element is in the viewport and auto-scrolls it if it is not.
|
||||
*/
|
||||
#ensureElementIsInTheViewportIfNeeded = async (
|
||||
element: ElementHandle,
|
||||
#ensureElementIsInTheViewportIfNeeded = async <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#ensureElementIsInTheViewport) {
|
||||
@ -332,8 +385,8 @@ export class LocatorImpl extends Locator {
|
||||
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
|
||||
* 'hidden' means the opposite of that.
|
||||
*/
|
||||
#waitForVisibilityIfNeeded = async (
|
||||
element: ElementHandle,
|
||||
#waitForVisibilityIfNeeded = async <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (this.#visibility === null) {
|
||||
@ -350,11 +403,11 @@ export class LocatorImpl extends Locator {
|
||||
};
|
||||
|
||||
/**
|
||||
* If the element is a button, textarea, input or select, wait till the
|
||||
* element becomes enabled.
|
||||
* If the element has a "disabled" property, wait for the element to be
|
||||
* enabled.
|
||||
*/
|
||||
#waitForEnabledIfNeeded = async (
|
||||
element: ElementHandle,
|
||||
#waitForEnabledIfNeeded = async <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#waitForEnabled) {
|
||||
@ -362,8 +415,8 @@ export class LocatorImpl extends Locator {
|
||||
}
|
||||
await this.#pageOrFrame.waitForFunction(
|
||||
el => {
|
||||
if (['button', 'textarea', 'input', 'select'].includes(el.tagName)) {
|
||||
return !(el as HTMLInputElement).disabled;
|
||||
if ('disabled' in el && typeof el.disabled === 'boolean') {
|
||||
return !el.disabled;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@ -379,8 +432,8 @@ export class LocatorImpl extends Locator {
|
||||
* Compares the bounding box of the element for two consecutive animation
|
||||
* frames and waits till they are the same.
|
||||
*/
|
||||
#waitForStableBoundingBoxIfNeeded = async (
|
||||
element: ElementHandle,
|
||||
#waitForStableBoundingBoxIfNeeded = async <ElementType extends Element>(
|
||||
element: HandleFor<ElementType>,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#waitForStableBoundingBox) {
|
||||
@ -423,21 +476,26 @@ export class LocatorImpl extends Locator {
|
||||
}, signal);
|
||||
};
|
||||
|
||||
async #run(
|
||||
action: (el: ElementHandle) => Promise<void>,
|
||||
options?: ActionOptions
|
||||
#run(
|
||||
action: (el: HandleFor<T>) => Promise<void>,
|
||||
signal?: AbortSignal,
|
||||
conditions: Array<ActionCondition<T>> = []
|
||||
) {
|
||||
await this.#waitForFunction(
|
||||
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(
|
||||
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) {
|
||||
return false;
|
||||
@ -446,9 +504,9 @@ export class LocatorImpl extends Locator {
|
||||
signal?.throwIfAborted();
|
||||
// 2. Perform action specific checks.
|
||||
await Promise.all(
|
||||
options?.conditions.map(check => {
|
||||
allConditions.map(check => {
|
||||
return check(element, signal);
|
||||
}) || []
|
||||
})
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
// 3. Perform the action
|
||||
@ -459,29 +517,26 @@ export class LocatorImpl extends Locator {
|
||||
void element.dispose().catch(debugError);
|
||||
}
|
||||
},
|
||||
options?.signal,
|
||||
signal,
|
||||
this.#timeout
|
||||
);
|
||||
}
|
||||
|
||||
async click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
async click<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
async element => {
|
||||
await element.click(clickOptions);
|
||||
await element.click(options);
|
||||
},
|
||||
{
|
||||
signal: clickOptions?.signal,
|
||||
conditions: [
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForEnabledIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
],
|
||||
}
|
||||
options?.signal,
|
||||
[
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForEnabledIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@ -491,13 +546,14 @@ export class LocatorImpl extends Locator {
|
||||
* method is chosen based on the type. contenteditable, selector, inputs are
|
||||
* supported.
|
||||
*/
|
||||
async fill(
|
||||
fill<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
value: string,
|
||||
fillOptions?: {signal?: AbortSignal}
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
return this.#run(
|
||||
async element => {
|
||||
const input = element as ElementHandle<HTMLElement>;
|
||||
const input = element as unknown as ElementHandle<HTMLElement>;
|
||||
const inputType = await input.evaluate(el => {
|
||||
if (el instanceof HTMLSelectElement) {
|
||||
return 'select';
|
||||
@ -583,40 +639,38 @@ export class LocatorImpl extends Locator {
|
||||
throw new Error(`Element cannot be filled out.`);
|
||||
}
|
||||
},
|
||||
{
|
||||
signal: fillOptions?.signal,
|
||||
conditions: [
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForEnabledIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
],
|
||||
}
|
||||
options?.signal,
|
||||
[
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForEnabledIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async hover(hoverOptions?: {signal?: AbortSignal}): Promise<void> {
|
||||
return await this.#run(
|
||||
hover<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return this.#run(
|
||||
async element => {
|
||||
await element.hover();
|
||||
},
|
||||
{
|
||||
signal: hoverOptions?.signal,
|
||||
conditions: [
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
],
|
||||
}
|
||||
options?.signal,
|
||||
[
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
return await this.#run(
|
||||
scroll<ElementType extends Element>(
|
||||
this: NodeLocator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void> {
|
||||
return this.#run(
|
||||
async element => {
|
||||
await element.evaluate(
|
||||
(el, scrollTop, scrollLeft) => {
|
||||
@ -627,29 +681,110 @@ export class LocatorImpl extends Locator {
|
||||
el.scrollLeft = scrollLeft;
|
||||
}
|
||||
},
|
||||
scrollOptions?.scrollTop,
|
||||
scrollOptions?.scrollLeft
|
||||
options?.scrollTop,
|
||||
options?.scrollLeft
|
||||
);
|
||||
},
|
||||
{
|
||||
signal: scrollOptions?.signal,
|
||||
conditions: [
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
],
|
||||
}
|
||||
options?.signal,
|
||||
[
|
||||
this.#ensureElementIsInTheViewportIfNeeded,
|
||||
this.#waitForVisibilityIfNeeded,
|
||||
this.#waitForStableBoundingBoxIfNeeded,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExpectedLocator<From, To extends From> extends Locator<To> {
|
||||
#base: Locator<From>;
|
||||
#predicate: Predicate<From, To>;
|
||||
|
||||
constructor(base: Locator<From>, predicate: Predicate<From, To>) {
|
||||
super();
|
||||
|
||||
this.#base = base;
|
||||
this.#predicate = predicate;
|
||||
}
|
||||
|
||||
override setVisibility(visibility: VisibilityOption): this {
|
||||
this.#base.setVisibility(visibility);
|
||||
return this;
|
||||
}
|
||||
override setTimeout(timeout: number): this {
|
||||
this.#base.setTimeout(timeout);
|
||||
return this;
|
||||
}
|
||||
override setEnsureElementIsInTheViewport(value: boolean): this {
|
||||
this.#base.setEnsureElementIsInTheViewport(value);
|
||||
return this;
|
||||
}
|
||||
override setWaitForEnabled(value: boolean): this {
|
||||
this.#base.setWaitForEnabled(value);
|
||||
return this;
|
||||
}
|
||||
override setWaitForStableBoundingBox(value: boolean): this {
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RaceLocatorImpl extends Locator {
|
||||
#locators: Locator[];
|
||||
class RaceLocator<T> extends Locator<T> {
|
||||
#locators: Array<Locator<T>>;
|
||||
|
||||
constructor(locators: Locator[]) {
|
||||
constructor(locators: Array<Locator<T>>) {
|
||||
super();
|
||||
this.#locators = locators;
|
||||
}
|
||||
@ -689,22 +824,20 @@ class RaceLocatorImpl extends Locator {
|
||||
return this;
|
||||
}
|
||||
|
||||
async #runRace(
|
||||
action: (el: Locator, abortSignal: AbortSignal) => Promise<void>,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
async #run(
|
||||
action: (locator: Locator<T>, signal: AbortSignal) => Promise<void>,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const abortControllers = new WeakMap<Locator, AbortController>();
|
||||
const abortControllers = new WeakMap<Locator<T>, AbortController>();
|
||||
|
||||
// Abort all locators if the user-provided signal aborts.
|
||||
options.signal?.addEventListener('abort', () => {
|
||||
signal?.addEventListener('abort', () => {
|
||||
for (const locator of this.#locators) {
|
||||
abortControllers.get(locator)?.abort();
|
||||
}
|
||||
});
|
||||
|
||||
const handleLocatorAction = (locator: Locator): (() => void) => {
|
||||
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) {
|
||||
@ -716,7 +849,7 @@ class RaceLocatorImpl extends Locator {
|
||||
};
|
||||
};
|
||||
|
||||
const createAbortController = (locator: Locator): AbortController => {
|
||||
const createAbortController = (locator: Locator<T>): AbortController => {
|
||||
const abortController = new AbortController();
|
||||
abortControllers.set(locator, abortController);
|
||||
return abortController;
|
||||
@ -731,7 +864,7 @@ class RaceLocatorImpl extends Locator {
|
||||
})
|
||||
);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
signal?.throwIfAborted();
|
||||
|
||||
const rejected = results.filter(
|
||||
(result): result is PromiseRejectedResult => {
|
||||
@ -754,70 +887,52 @@ class RaceLocatorImpl extends Locator {
|
||||
}
|
||||
}
|
||||
|
||||
override async click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
async click<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.click({
|
||||
...clickOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.click({...options, signal});
|
||||
},
|
||||
{
|
||||
signal: clickOptions?.signal,
|
||||
}
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
override async fill(
|
||||
async fill<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
value: string,
|
||||
fillOptions?: {signal?: AbortSignal}
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.fill(value, {
|
||||
...fillOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.fill(value, {...options, signal});
|
||||
},
|
||||
{
|
||||
signal: fillOptions?.signal,
|
||||
}
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
override async hover(hoverOptions?: {signal?: AbortSignal}): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.hover({
|
||||
...hoverOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
async hover<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.hover({...options, signal});
|
||||
},
|
||||
{
|
||||
signal: hoverOptions?.signal,
|
||||
}
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
|
||||
override async scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.scroll({
|
||||
...scrollOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
async scroll<ElementType extends Element>(
|
||||
this: RaceLocator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
): Promise<void> {
|
||||
return await this.#run(
|
||||
(locator, signal) => {
|
||||
return locator.scroll({...options, signal});
|
||||
},
|
||||
{
|
||||
signal: scrollOptions?.signal,
|
||||
}
|
||||
options?.signal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -831,7 +831,9 @@ export class Page extends EventEmitter {
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
*/
|
||||
locator(selector: string): Locator {
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
): Locator<NodeFor<Selector>> {
|
||||
return Locator.create(this, selector);
|
||||
}
|
||||
|
||||
@ -840,7 +842,7 @@ export class Page extends EventEmitter {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
locatorRace(locators: Locator[]): Locator {
|
||||
locatorRace(locators: Array<Locator<Node>>): Locator<Node> {
|
||||
return Locator.race(locators);
|
||||
}
|
||||
|
||||
|
@ -2399,6 +2399,12 @@
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[locator.spec] Locator Locator.prototype.expect should resolve as soon as the predicate matches",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["TIMEOUT"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[mouse.spec] Mouse should reset properly",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
@ -538,4 +538,59 @@ describe('Locator', function () {
|
||||
await expect(result).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Locator.prototype.expect', () => {
|
||||
it('should not resolve if the predicate does not match', async () => {
|
||||
const clock = sinon.useFakeTimers({
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
try {
|
||||
const {page} = await getTestState();
|
||||
page.setDefaultTimeout(5000);
|
||||
await page.setContent(`<div>test</div>`);
|
||||
const result = page
|
||||
.locator('::-p-text(test)')
|
||||
.expect((element): Promise<boolean> => {
|
||||
return Promise.resolve(
|
||||
element.getAttribute('clickable') === 'true'
|
||||
);
|
||||
})
|
||||
.hover();
|
||||
clock.tick(5100);
|
||||
await expect(result).rejects.toEqual(
|
||||
new TimeoutError('waitForFunction timed out. The timeout is 5000ms.')
|
||||
);
|
||||
} finally {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should resolve as soon as the predicate matches', async () => {
|
||||
const clock = sinon.useFakeTimers({
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
try {
|
||||
const {page} = await getTestState();
|
||||
page.setDefaultTimeout(5000);
|
||||
await page.setContent(`<div>test</div>`);
|
||||
const result = page
|
||||
.locator('::-p-text(test)')
|
||||
.expect(async element => {
|
||||
return element.getAttribute('clickable') === 'true';
|
||||
})
|
||||
.expect(element => {
|
||||
return element.getAttribute('clickable') === 'true';
|
||||
})
|
||||
.hover();
|
||||
clock.tick(2000);
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('div')?.setAttribute('clickable', 'true');
|
||||
});
|
||||
clock.tick(2000);
|
||||
await expect(result).resolves.toEqual(undefined);
|
||||
} finally {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user