feat: implement Locator.race (#10337)
This commit is contained in:
parent
dde569b97d
commit
9c35e9ab1f
@ -8,7 +8,7 @@ sidebar_label: Locator.click
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
click(
|
||||
abstract click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ Fills out the input identified by the locator using the provided value. The type
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
fill(
|
||||
abstract fill(
|
||||
value: string,
|
||||
fillOptions?: {
|
||||
signal?: AbortSignal;
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.hover
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
hover(hoverOptions?: {signal?: AbortSignal}): Promise<void>;
|
||||
abstract hover(hoverOptions?: {signal?: AbortSignal}): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -9,24 +9,25 @@ Locators describe a strategy of locating elements and performing an action on th
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export declare class Locator extends EventEmitter
|
||||
export declare abstract class Locator extends EventEmitter
|
||||
```
|
||||
|
||||
**Extends:** [EventEmitter](./puppeteer.eventemitter.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) | | |
|
||||
| [off(eventName, handler)](./puppeteer.locator.off.md) | | |
|
||||
| [on(eventName, handler)](./puppeteer.locator.on.md) | | |
|
||||
| [once(eventName, handler)](./puppeteer.locator.once.md) | | |
|
||||
| [scroll(scrollOptions)](./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(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) | | |
|
||||
| [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) | | |
|
||||
| [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) | | |
|
||||
|
25
docs/api/puppeteer.locator.race.md
Normal file
25
docs/api/puppeteer.locator.race.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
sidebar_label: Locator.race
|
||||
---
|
||||
|
||||
# Locator.race() method
|
||||
|
||||
Creates a race between multiple locators but ensures that only a single one acts.
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
static race(locators: Locator[]): Locator;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------------------------------------- | ----------- |
|
||||
| locators | [Locator](./puppeteer.locator.md)\[\] | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
[Locator](./puppeteer.locator.md)
|
@ -8,7 +8,7 @@ sidebar_label: Locator.scroll
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
scroll(scrollOptions?: {
|
||||
abstract scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setEnsureElementIsInTheViewport
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setEnsureElementIsInTheViewport(value: boolean): this;
|
||||
abstract setEnsureElementIsInTheViewport(value: boolean): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setTimeout(timeout: number): this;
|
||||
abstract setTimeout(timeout: number): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setVisibility
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setVisibility(visibility: VisibilityOption): this;
|
||||
abstract setVisibility(visibility: VisibilityOption): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setWaitForEnabled
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setWaitForEnabled(value: boolean): this;
|
||||
abstract setWaitForEnabled(value: boolean): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setWaitForStableBoundingBox
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setWaitForStableBoundingBox(value: boolean): this;
|
||||
abstract setWaitForStableBoundingBox(value: boolean): this;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -116,30 +116,24 @@ export interface LocatorEventObject {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Locator extends EventEmitter {
|
||||
export abstract class Locator extends EventEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static create(pageOrFrame: Page | Frame, selector: string): Locator {
|
||||
return new Locator(pageOrFrame, selector).setTimeout(
|
||||
return new LocatorImpl(pageOrFrame, selector).setTimeout(
|
||||
'getDefaultTimeout' in pageOrFrame
|
||||
? pageOrFrame.getDefaultTimeout()
|
||||
: pageOrFrame.page().getDefaultTimeout()
|
||||
);
|
||||
}
|
||||
|
||||
#pageOrFrame: Page | Frame;
|
||||
#selector: string;
|
||||
#visibility: VisibilityOption = 'visible';
|
||||
#timeout = 30_000;
|
||||
#ensureElementIsInTheViewport = true;
|
||||
#waitForEnabled = true;
|
||||
#waitForStableBoundingBox = true;
|
||||
|
||||
private constructor(pageOrFrame: Page | Frame, selector: string) {
|
||||
super();
|
||||
this.#pageOrFrame = pageOrFrame;
|
||||
this.#selector = selector;
|
||||
/**
|
||||
* Creates a race between multiple locators but ensures that only a single one
|
||||
* acts.
|
||||
*/
|
||||
static race(locators: Locator[]): Locator {
|
||||
return new RaceLocatorImpl(locators);
|
||||
}
|
||||
|
||||
override on<K extends keyof LocatorEventObject>(
|
||||
@ -163,6 +157,60 @@ export class Locator extends EventEmitter {
|
||||
return super.off(eventName, handler);
|
||||
}
|
||||
|
||||
abstract setVisibility(visibility: VisibilityOption): this;
|
||||
|
||||
abstract setTimeout(timeout: number): this;
|
||||
|
||||
abstract setEnsureElementIsInTheViewport(value: boolean): this;
|
||||
|
||||
abstract setWaitForEnabled(value: boolean): this;
|
||||
|
||||
abstract setWaitForStableBoundingBox(value: boolean): this;
|
||||
|
||||
abstract click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
abstract fill(
|
||||
value: string,
|
||||
fillOptions?: {signal?: AbortSignal}
|
||||
): Promise<void>;
|
||||
|
||||
abstract hover(hoverOptions?: {signal?: AbortSignal}): Promise<void>;
|
||||
|
||||
abstract scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class LocatorImpl extends Locator {
|
||||
#pageOrFrame: Page | Frame;
|
||||
#selector: string;
|
||||
#visibility: VisibilityOption = 'visible';
|
||||
#timeout = 30_000;
|
||||
#ensureElementIsInTheViewport = true;
|
||||
#waitForEnabled = true;
|
||||
#waitForStableBoundingBox = true;
|
||||
|
||||
constructor(pageOrFrame: Page | Frame, selector: string) {
|
||||
super();
|
||||
this.#pageOrFrame = pageOrFrame;
|
||||
this.#selector = selector;
|
||||
}
|
||||
|
||||
setVisibility(visibility: VisibilityOption): this {
|
||||
this.#visibility = visibility;
|
||||
return this;
|
||||
@ -211,6 +259,7 @@ export class Locator extends EventEmitter {
|
||||
() => {
|
||||
controller?.abort();
|
||||
isActive = false;
|
||||
clearTimeout(timeoutId);
|
||||
},
|
||||
{once: true}
|
||||
);
|
||||
@ -593,3 +642,162 @@ export class Locator extends EventEmitter {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RaceLocatorImpl extends Locator {
|
||||
#locators: Locator[];
|
||||
|
||||
constructor(locators: Locator[]) {
|
||||
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 #runRace(
|
||||
action: (el: Locator, abortSignal: AbortSignal) => Promise<void>,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
) {
|
||||
const abortControllers = new WeakMap<Locator, AbortController>();
|
||||
|
||||
// Abort all locators if the user-provided signal aborts.
|
||||
options.signal?.addEventListener('abort', () => {
|
||||
for (const locator of this.#locators) {
|
||||
abortControllers.get(locator)?.abort();
|
||||
}
|
||||
});
|
||||
|
||||
const handleLocatorAction = (locator: Locator): (() => void) => {
|
||||
return () => {
|
||||
// When one locator is ready to act, we will abort other locators.
|
||||
for (const other of this.#locators) {
|
||||
if (other !== locator) {
|
||||
abortControllers.get(other)?.abort();
|
||||
}
|
||||
}
|
||||
this.emit(LocatorEmittedEvents.Action);
|
||||
};
|
||||
};
|
||||
|
||||
const createAbortController = (locator: Locator): AbortController => {
|
||||
const abortController = new AbortController();
|
||||
abortControllers.set(locator, abortController);
|
||||
return abortController;
|
||||
};
|
||||
|
||||
await Promise.allSettled(
|
||||
this.#locators.map(locator => {
|
||||
return action(
|
||||
locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)),
|
||||
createAbortController(locator).signal
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
}
|
||||
|
||||
override async click(
|
||||
clickOptions?: ClickOptions & {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.click({
|
||||
...clickOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
},
|
||||
{
|
||||
signal: clickOptions?.signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override async fill(
|
||||
value: string,
|
||||
fillOptions?: {signal?: AbortSignal}
|
||||
): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.fill(value, {
|
||||
...fillOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
},
|
||||
{
|
||||
signal: fillOptions?.signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override async hover(hoverOptions?: {signal?: AbortSignal}): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.hover({
|
||||
...hoverOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
},
|
||||
{
|
||||
signal: hoverOptions?.signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override async scroll(scrollOptions?: {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
return await this.#runRace(
|
||||
(locator, abortSignal) => {
|
||||
return locator.hover({
|
||||
...scrollOptions,
|
||||
signal: abortSignal,
|
||||
});
|
||||
},
|
||||
{
|
||||
signal: scrollOptions?.signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -395,6 +395,12 @@
|
||||
"parameters": ["chrome"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation \"after each\" hook for \"should work with both domcontentloaded and load\"",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -2482,11 +2488,5 @@
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["cdp", "chrome", "headless"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation \"after each\" hook for \"should work with both domcontentloaded and load\"",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
}
|
||||
]
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
import expect from 'expect';
|
||||
import {TimeoutError} from 'puppeteer-core';
|
||||
import {LocatorEmittedEvents} from 'puppeteer-core/internal/api/Locator.js';
|
||||
import {
|
||||
Locator,
|
||||
LocatorEmittedEvents,
|
||||
} from 'puppeteer-core/internal/api/Locator.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
@ -444,4 +447,53 @@ describe('Locator', function () {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Locator.race', () => {
|
||||
it('races multiple locators', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
<button onclick="window.count++;">test</button>
|
||||
`);
|
||||
await page.evaluate(() => {
|
||||
// @ts-expect-error different context.
|
||||
window.count = 0;
|
||||
});
|
||||
await Locator.race([
|
||||
page.locator('button'),
|
||||
page.locator('button'),
|
||||
]).click();
|
||||
const count = await page.evaluate(() => {
|
||||
// @ts-expect-error different context.
|
||||
return globalThis.count;
|
||||
});
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it('can be aborted', async () => {
|
||||
const {page} = getTestState();
|
||||
const clock = sinon.useFakeTimers();
|
||||
try {
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
||||
`);
|
||||
const abortController = new AbortController();
|
||||
const result = Locator.race([
|
||||
page.locator('button'),
|
||||
page.locator('button'),
|
||||
])
|
||||
.setTimeout(5000)
|
||||
.click({
|
||||
signal: abortController.signal,
|
||||
});
|
||||
clock.tick(2000);
|
||||
abortController.abort();
|
||||
await expect(result).rejects.toThrow(/aborted/);
|
||||
} finally {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user