From 0715ad828162dd28e98c2a9b82f63c1a3f859f55 Mon Sep 17 00:00:00 2001
From: jrandolf <101637635+jrandolf@users.noreply.github.com>
Date: Thu, 20 Jul 2023 15:06:42 +0200
Subject: [PATCH] refactor: move locators to separate files (#10591)
---
docs/api/index.md | 1 +
docs/api/puppeteer.locator.race.md | 2 +-
docs/api/puppeteer.unionlocatorof.md | 13 +
packages/puppeteer-core/src/api/Frame.ts | 4 +-
packages/puppeteer-core/src/api/Page.ts | 6 +-
packages/puppeteer-core/src/api/api.ts | 2 +-
.../src/api/locators/ExpectedLocator.ts | 106 ++++
.../src/api/locators/Locator.ts | 201 ++++++++
.../{Locator.ts => locators/NodeLocator.ts} | 481 +-----------------
.../src/api/locators/RaceLocator.ts | 174 +++++++
.../src/api/locators/locators.ts | 4 +
test/src/locator.spec.ts | 2 +-
12 files changed, 527 insertions(+), 469 deletions(-)
create mode 100644 docs/api/puppeteer.unionlocatorof.md
create mode 100644 packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
create mode 100644 packages/puppeteer-core/src/api/locators/Locator.ts
rename packages/puppeteer-core/src/api/{Locator.ts => locators/NodeLocator.ts} (52%)
create mode 100644 packages/puppeteer-core/src/api/locators/RaceLocator.ts
create mode 100644 packages/puppeteer-core/src/api/locators/locators.ts
diff --git a/docs/api/index.md b/docs/api/index.md
index a849c4e6..b391efc2 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -181,4 +181,5 @@ sidebar_label: API
| [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | Utility type exposed to enable users to define options that can be passed to puppeteer.launch
without having to list the set of all types. |
| [ResourceType](./puppeteer.resourcetype.md) | Resource types for HTTPRequests as perceived by the rendering engine. |
| [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | |
+| [UnionLocatorOf](./puppeteer.unionlocatorof.md) | |
| [VisibilityOption](./puppeteer.visibilityoption.md) | |
diff --git a/docs/api/puppeteer.locator.race.md b/docs/api/puppeteer.locator.race.md
index b6055a8a..642df259 100644
--- a/docs/api/puppeteer.locator.race.md
+++ b/docs/api/puppeteer.locator.race.md
@@ -24,4 +24,4 @@ class Locator {
**Returns:**
-[Locator](./puppeteer.locator.md)<UnionLocatorOf<Locators>>
+[Locator](./puppeteer.locator.md)<[UnionLocatorOf](./puppeteer.unionlocatorof.md)<Locators>>
diff --git a/docs/api/puppeteer.unionlocatorof.md b/docs/api/puppeteer.unionlocatorof.md
new file mode 100644
index 00000000..ea442953
--- /dev/null
+++ b/docs/api/puppeteer.unionlocatorof.md
@@ -0,0 +1,13 @@
+---
+sidebar_label: UnionLocatorOf
+---
+
+# UnionLocatorOf type
+
+#### Signature:
+
+```typescript
+export type UnionLocatorOf = T extends Array> ? S : never;
+```
+
+**References:** [Locator](./puppeteer.locator.md)
diff --git a/packages/puppeteer-core/src/api/Frame.ts b/packages/puppeteer-core/src/api/Frame.ts
index 9ff895f4..c5e56872 100644
--- a/packages/puppeteer-core/src/api/Frame.ts
+++ b/packages/puppeteer-core/src/api/Frame.ts
@@ -39,7 +39,7 @@ import {TaskManager} from '../common/WaitTask.js';
import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js';
-import {Locator} from './Locator.js';
+import {Locator, NodeLocator} from './locators/locators.js';
/**
* @internal
@@ -427,7 +427,7 @@ export class Frame {
locator(
selector: Selector
): Locator> {
- return Locator.create(this, selector);
+ return NodeLocator.create(this, selector);
}
/**
diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts
index f073a275..386ab123 100644
--- a/packages/puppeteer-core/src/api/Page.ts
+++ b/packages/puppeteer-core/src/api/Page.ts
@@ -71,9 +71,9 @@ import type {
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
-import {Keyboard, Mouse, Touchscreen, KeyboardTypeOptions} from './Input.js';
+import {Keyboard, KeyboardTypeOptions, Mouse, Touchscreen} from './Input.js';
import type {JSHandle} from './JSHandle.js';
-import {Locator} from './Locator.js';
+import {Locator, NodeLocator} from './locators/locators.js';
/**
* @public
@@ -834,7 +834,7 @@ export class Page extends EventEmitter {
locator(
selector: Selector
): Locator> {
- return Locator.create(this, selector);
+ return NodeLocator.create(this, selector);
}
/**
diff --git a/packages/puppeteer-core/src/api/api.ts b/packages/puppeteer-core/src/api/api.ts
index f4bcd2ef..5ad76682 100644
--- a/packages/puppeteer-core/src/api/api.ts
+++ b/packages/puppeteer-core/src/api/api.ts
@@ -23,4 +23,4 @@ export * from './Input.js';
export * from './Frame.js';
export * from './HTTPResponse.js';
export * from './HTTPRequest.js';
-export * from './Locator.js';
+export * from './locators/locators.js';
diff --git a/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
new file mode 100644
index 00000000..b11571a0
--- /dev/null
+++ b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts
@@ -0,0 +1,106 @@
+import {Awaitable} from '../../common/common.js';
+import {ElementHandle} from '../ElementHandle.js';
+
+import {
+ ActionOptions,
+ LOCATOR_CONTEXTS,
+ Locator,
+ LocatorClickOptions,
+ LocatorContext,
+ LocatorScrollOptions,
+ VisibilityOption,
+ type ActionCondition,
+} from './locators.js';
+
+/**
+ * @public
+ */
+export type Predicate =
+ | ((value: From) => value is To)
+ | ((value: From) => Awaitable);
+
+/**
+ * @internal
+ */
+export class ExpectedLocator extends Locator {
+ #base: Locator;
+ #predicate: Predicate;
+
+ constructor(base: Locator, predicate: Predicate) {
+ 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 = async (handle, signal) => {
+ // TODO(jrandolf): We should remove this once JSHandle has waitForFunction.
+ await (handle as ElementHandle).frame.waitForFunction(
+ this.#predicate,
+ {signal},
+ handle
+ );
+ };
+
+ #insertFilterCondition<
+ FromElement extends Node,
+ ToElement extends FromElement,
+ >(this: ExpectedLocator): void {
+ const context = (LOCATOR_CONTEXTS.get(this.#base) ??
+ {}) as LocatorContext;
+ context.conditions ??= new Set();
+ context.conditions.add(this.#condition);
+ LOCATOR_CONTEXTS.set(this.#base, context);
+ }
+
+ override click(
+ this: ExpectedLocator,
+ options?: Readonly
+ ): Promise {
+ this.#insertFilterCondition();
+ return this.#base.click(options);
+ }
+ override fill(
+ this: ExpectedLocator,
+ value: string,
+ options?: Readonly
+ ): Promise {
+ this.#insertFilterCondition();
+ return this.#base.fill(value, options);
+ }
+ override hover(
+ this: ExpectedLocator,
+ options?: Readonly
+ ): Promise {
+ this.#insertFilterCondition();
+ return this.#base.hover(options);
+ }
+ override scroll(
+ this: ExpectedLocator,
+ options?: Readonly
+ ): Promise {
+ this.#insertFilterCondition();
+ return this.#base.scroll(options);
+ }
+}
diff --git a/packages/puppeteer-core/src/api/locators/Locator.ts b/packages/puppeteer-core/src/api/locators/Locator.ts
new file mode 100644
index 00000000..3d8e99f0
--- /dev/null
+++ b/packages/puppeteer-core/src/api/locators/Locator.ts
@@ -0,0 +1,201 @@
+import {EventEmitter} from '../../common/EventEmitter.js';
+import {ClickOptions} from '../ElementHandle.js';
+
+import {
+ ExpectedLocator,
+ Predicate,
+ RaceLocator,
+ UnionLocatorOf,
+ type ActionCondition,
+} from './locators.js';
+
+/**
+ * @internal
+ */
+export interface LocatorContext {
+ conditions?: Set>;
+}
+
+/**
+ * @internal
+ */
+export const LOCATOR_CONTEXTS = new WeakMap<
+ Locator,
+ LocatorContext
+>();
+
+/**
+ * @public
+ */
+export type VisibilityOption = 'hidden' | 'visible' | null;
+
+/**
+ * @public
+ */
+export interface LocatorOptions {
+ /**
+ * Whether to wait for the element to be `visible` or `hidden`. `null` to
+ * disable visibility checks.
+ */
+ visibility: VisibilityOption;
+ /**
+ * Total timeout for the entire locator operation.
+ *
+ * Pass `0` to disable timeout.
+ *
+ * @defaultValue `Page.getDefaultTimeout()`
+ */
+ timeout: number;
+ /**
+ * Whether to scroll the element into viewport if not in the viewprot already.
+ * @defaultValue `true`
+ */
+ ensureElementIsInTheViewport: boolean;
+ /**
+ * Whether to wait for input elements to become enabled before the action.
+ * Applicable to `click` and `fill` actions.
+ * @defaultValue `true`
+ */
+ waitForEnabled: boolean;
+ /**
+ * Whether to wait for the element's bounding box to be same between two
+ * animation frames.
+ * @defaultValue `true`
+ */
+ waitForStableBoundingBox: boolean;
+}
+
+/**
+ * @public
+ */
+export interface ActionOptions {
+ signal?: AbortSignal;
+}
+
+/**
+ * @public
+ */
+export type LocatorClickOptions = ClickOptions & ActionOptions;
+
+/**
+ * @public
+ */
+export interface LocatorScrollOptions extends ActionOptions {
+ scrollTop?: number;
+ scrollLeft?: number;
+}
+
+/**
+ * All the events that a locator instance may emit.
+ *
+ * @public
+ */
+export enum LocatorEmittedEvents {
+ /**
+ * Emitted every time before the locator performs an action on the located element(s).
+ */
+ Action = 'action',
+}
+
+/**
+ * @public
+ */
+export interface LocatorEventObject {
+ [LocatorEmittedEvents.Action]: 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,
+ * the whole operation is retried. Various preconditions for a successful action
+ * are checked automatically.
+ *
+ * @public
+ */
+export abstract class Locator extends EventEmitter {
+ /**
+ * Used for nominally typing {@link Locator}.
+ */
+ declare _?: T;
+
+ /**
+ * Creates a race between multiple locators but ensures that only a single one
+ * acts.
+ */
+ static race>>(
+ locators: Locators
+ ): Locator> {
+ return new RaceLocator(
+ locators as Array>>
+ );
+ }
+
+ /**
+ * Creates an expectation that is evaluated against located values.
+ *
+ * If the expectations do not match, then the locator will retry.
+ *
+ * @internal
+ */
+ expect(predicate: Predicate): Locator {
+ return new ExpectedLocator(this, predicate);
+ }
+
+ override on(
+ eventName: K,
+ handler: (event: LocatorEventObject[K]) => void
+ ): this {
+ return super.on(eventName, handler);
+ }
+
+ override once(
+ eventName: K,
+ handler: (event: LocatorEventObject[K]) => void
+ ): this {
+ return super.once(eventName, handler);
+ }
+
+ override off(
+ eventName: K,
+ handler: (event: LocatorEventObject[K]) => void
+ ): this {
+ 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(
+ this: Locator,
+ options?: Readonly
+ ): Promise;
+
+ /**
+ * 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(
+ this: Locator,
+ value: string,
+ options?: Readonly
+ ): Promise;
+
+ abstract hover(
+ this: Locator,
+ options?: Readonly
+ ): Promise;
+
+ abstract scroll(
+ this: Locator,
+ options?: Readonly
+ ): Promise;
+}
diff --git a/packages/puppeteer-core/src/api/Locator.ts b/packages/puppeteer-core/src/api/locators/NodeLocator.ts
similarity index 52%
rename from packages/puppeteer-core/src/api/Locator.ts
rename to packages/puppeteer-core/src/api/locators/NodeLocator.ts
index 5ad079bf..7e87c938 100644
--- a/packages/puppeteer-core/src/api/Locator.ts
+++ b/packages/puppeteer-core/src/api/locators/NodeLocator.ts
@@ -1,75 +1,20 @@
-/**
- * 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 {TimeoutError} from '../../common/Errors.js';
+import {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';
-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 {BoundingBox, ClickOptions, ElementHandle} from './ElementHandle.js';
-import type {Frame} from './Frame.js';
-import type {Page} from './Page.js';
-
-interface LocatorContext {
- conditions?: Set>;
-}
-
-const LOCATOR_CONTEXTS = new WeakMap, LocatorContext>();
-
-/**
- * @public
- */
-export type VisibilityOption = 'hidden' | 'visible' | null;
-
-/**
- * @public
- */
-export interface LocatorOptions {
- /**
- * Whether to wait for the element to be `visible` or `hidden`. `null` to
- * disable visibility checks.
- */
- visibility: VisibilityOption;
- /**
- * Total timeout for the entire locator operation.
- *
- * Pass `0` to disable timeout.
- *
- * @defaultValue `Page.getDefaultTimeout()`
- */
- timeout: number;
- /**
- * Whether to scroll the element into viewport if not in the viewprot already.
- * @defaultValue `true`
- */
- ensureElementIsInTheViewport: boolean;
- /**
- * Whether to wait for input elements to become enabled before the action.
- * Applicable to `click` and `fill` actions.
- * @defaultValue `true`
- */
- waitForEnabled: boolean;
- /**
- * Whether to wait for the element's bounding box to be same between two
- * animation frames.
- * @defaultValue `true`
- */
- waitForStableBoundingBox: boolean;
-}
+import {
+ ActionOptions,
+ LOCATOR_CONTEXTS,
+ Locator,
+ LocatorClickOptions,
+ LocatorEmittedEvents,
+ LocatorScrollOptions,
+ VisibilityOption,
+} from './locators.js';
/**
* Timeout for individual operations inside the locator. On errors the
@@ -77,7 +22,7 @@ export interface LocatorOptions {
* exceeded. This timeout should be generally much lower as locating an
* element means multiple asynchronious operations.
*/
-const CONDITION_TIMEOUT = 1_000;
+const CONDITION_TIMEOUT = 1000;
const WAIT_FOR_FUNCTION_DELAY = 100;
/**
@@ -89,67 +34,9 @@ export type ActionCondition = (
) => Promise;
/**
- * @public
+ * @internal
*/
-export type Predicate =
- | ((value: From) => value is To)
- | ((value: From) => Awaitable);
-
-/**
- * @public
- */
-export interface ActionOptions {
- signal?: AbortSignal;
-}
-
-/**
- * @public
- */
-export type LocatorClickOptions = ClickOptions & ActionOptions;
-
-/**
- * @public
- */
-export interface LocatorScrollOptions extends ActionOptions {
- scrollTop?: number;
- scrollLeft?: number;
-}
-
-/**
- * All the events that a locator instance may emit.
- *
- * @public
- */
-export enum LocatorEmittedEvents {
- /**
- * Emitted every time before the locator performs an action on the located element(s).
- */
- Action = 'action',
-}
-
-/**
- * @public
- */
-export interface LocatorEventObject {
- [LocatorEmittedEvents.Action]: never;
-}
-
-type UnionLocatorOf = T extends Array> ? 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,
- * the whole operation is retried. Various preconditions for a successful action
- * are checked automatically.
- *
- * @public
- */
-export abstract class Locator extends EventEmitter {
- /**
- * Used for nominally typing {@link Locator}.
- */
- declare _?: T;
-
+export class NodeLocator extends Locator {
/**
* @internal
*/
@@ -164,96 +51,10 @@ export abstract class Locator extends EventEmitter {
);
}
- /**
- * Creates a race between multiple locators but ensures that only a single one
- * acts.
- */
- static race>>(
- locators: Locators
- ): Locator> {
- return new RaceLocator(
- locators as Array>>
- );
- }
-
- /**
- * Creates an expectation that is evaluated against located values.
- *
- * If the expectations do not match, then the locator will retry.
- *
- * @internal
- */
- expect(predicate: Predicate): Locator {
- return new ExpectedLocator(this, predicate);
- }
-
- override on(
- eventName: K,
- handler: (event: LocatorEventObject[K]) => void
- ): this {
- return super.on(eventName, handler);
- }
-
- override once(
- eventName: K,
- handler: (event: LocatorEventObject[K]) => void
- ): this {
- return super.once(eventName, handler);
- }
-
- override off(
- eventName: K,
- handler: (event: LocatorEventObject[K]) => void
- ): this {
- 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(
- this: Locator,
- options?: Readonly
- ): Promise;
-
- /**
- * 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(
- this: Locator,
- value: string,
- options?: Readonly
- ): Promise;
-
- abstract hover(
- this: Locator,
- options?: Readonly
- ): Promise;
-
- abstract scroll(
- this: Locator,
- options?: Readonly
- ): Promise;
-}
-
-/**
- * @internal
- */
-export class NodeLocator extends Locator {
#pageOrFrame: Page | Frame;
#selector: string;
#visibility: VisibilityOption = 'visible';
- #timeout = 30_000;
+ #timeout = 30000;
#ensureElementIsInTheViewport = true;
#waitForEnabled = true;
#waitForStableBoundingBox = true;
@@ -694,245 +495,3 @@ export class NodeLocator extends Locator {
);
}
}
-
-class ExpectedLocator extends Locator {
- #base: Locator;
- #predicate: Predicate;
-
- constructor(base: Locator, predicate: Predicate) {
- 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 = async (handle, signal) => {
- // TODO(jrandolf): We should remove this once JSHandle has waitForFunction.
- await (handle as ElementHandle).frame.waitForFunction(
- this.#predicate,
- {signal},
- handle
- );
- };
-
- #insertFilterCondition<
- FromElement extends Node,
- ToElement extends FromElement,
- >(this: ExpectedLocator): void {
- const context = (LOCATOR_CONTEXTS.get(this.#base) ??
- {}) as LocatorContext;
- context.conditions ??= new Set();
- context.conditions.add(this.#condition);
- LOCATOR_CONTEXTS.set(this.#base, context);
- }
-
- override click(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.click(options);
- }
- override fill(
- this: ExpectedLocator,
- value: string,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.fill(value, options);
- }
- override hover(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.hover(options);
- }
- override scroll(
- this: ExpectedLocator,
- options?: Readonly
- ): Promise {
- this.#insertFilterCondition();
- return this.#base.scroll(options);
- }
-}
-
-/**
- * @internal
- */
-class RaceLocator extends Locator {
- #locators: Array>;
-
- constructor(locators: Array>) {
- super();
- this.#locators = locators;
- }
-
- override setVisibility(visibility: VisibilityOption): this {
- for (const locator of this.#locators) {
- locator.setVisibility(visibility);
- }
- return this;
- }
-
- override setTimeout(timeout: number): this {
- for (const locator of this.#locators) {
- locator.setTimeout(timeout);
- }
- return this;
- }
-
- override setEnsureElementIsInTheViewport(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setEnsureElementIsInTheViewport(value);
- }
- return this;
- }
-
- override setWaitForEnabled(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setWaitForEnabled(value);
- }
- return this;
- }
-
- override setWaitForStableBoundingBox(value: boolean): this {
- for (const locator of this.#locators) {
- locator.setWaitForStableBoundingBox(value);
- }
- return this;
- }
-
- async #run(
- action: (locator: Locator, signal: AbortSignal) => Promise,
- signal?: AbortSignal
- ) {
- const abortControllers = new WeakMap, AbortController>();
-
- // Abort all locators if the user-provided signal aborts.
- signal?.addEventListener('abort', () => {
- for (const locator of this.#locators) {
- abortControllers.get(locator)?.abort();
- }
- });
-
- const handleLocatorAction = (locator: Locator): (() => void) => {
- return () => {
- // When one locator is ready to act, we will abort other locators.
- for (const other of this.#locators) {
- if (other !== locator) {
- abortControllers.get(other)?.abort();
- }
- }
- this.emit(LocatorEmittedEvents.Action);
- };
- };
-
- const createAbortController = (locator: Locator): AbortController => {
- const abortController = new AbortController();
- abortControllers.set(locator, abortController);
- return abortController;
- };
-
- const results = await Promise.allSettled(
- this.#locators.map(locator => {
- return action(
- locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)),
- createAbortController(locator).signal
- );
- })
- );
-
- signal?.throwIfAborted();
-
- const rejected = results.filter(
- (result): result is PromiseRejectedResult => {
- return result.status === 'rejected';
- }
- );
-
- // If some locators are fulfilled, do not throw.
- if (rejected.length !== results.length) {
- return;
- }
-
- for (const result of rejected) {
- const reason = result.reason;
- // AbortError is be an expected result of a race.
- if (isErrorLike(reason) && reason.name === 'AbortError') {
- continue;
- }
- throw reason;
- }
- }
-
- async click(
- this: RaceLocator,
- options?: Readonly
- ): Promise {
- return await this.#run(
- (locator, signal) => {
- return locator.click({...options, signal});
- },
- options?.signal
- );
- }
-
- async fill(
- this: RaceLocator,
- value: string,
- options?: Readonly
- ): Promise {
- return await this.#run(
- (locator, signal) => {
- return locator.fill(value, {...options, signal});
- },
- options?.signal
- );
- }
-
- async hover(
- this: RaceLocator,
- options?: Readonly
- ): Promise {
- return await this.#run(
- (locator, signal) => {
- return locator.hover({...options, signal});
- },
- options?.signal
- );
- }
-
- async scroll(
- this: RaceLocator,
- options?: Readonly
- ): Promise {
- return await this.#run(
- (locator, signal) => {
- return locator.scroll({...options, signal});
- },
- options?.signal
- );
- }
-}
diff --git a/packages/puppeteer-core/src/api/locators/RaceLocator.ts b/packages/puppeteer-core/src/api/locators/RaceLocator.ts
new file mode 100644
index 00000000..6fd5aeca
--- /dev/null
+++ b/packages/puppeteer-core/src/api/locators/RaceLocator.ts
@@ -0,0 +1,174 @@
+import {isErrorLike} from '../../util/ErrorLike.js';
+
+import {
+ Locator,
+ VisibilityOption,
+ LocatorEmittedEvents,
+ LocatorClickOptions,
+ ActionOptions,
+ LocatorScrollOptions,
+} from './locators.js';
+
+/**
+ * @public
+ */
+export type UnionLocatorOf = T extends Array> ? S : never;
+
+/**
+ * @internal
+ */
+export class RaceLocator extends Locator {
+ #locators: Array>;
+
+ constructor(locators: Array>) {
+ super();
+ this.#locators = locators;
+ }
+
+ override setVisibility(visibility: VisibilityOption): this {
+ for (const locator of this.#locators) {
+ locator.setVisibility(visibility);
+ }
+ return this;
+ }
+
+ override setTimeout(timeout: number): this {
+ for (const locator of this.#locators) {
+ locator.setTimeout(timeout);
+ }
+ return this;
+ }
+
+ override setEnsureElementIsInTheViewport(value: boolean): this {
+ for (const locator of this.#locators) {
+ locator.setEnsureElementIsInTheViewport(value);
+ }
+ return this;
+ }
+
+ override setWaitForEnabled(value: boolean): this {
+ for (const locator of this.#locators) {
+ locator.setWaitForEnabled(value);
+ }
+ return this;
+ }
+
+ override setWaitForStableBoundingBox(value: boolean): this {
+ for (const locator of this.#locators) {
+ locator.setWaitForStableBoundingBox(value);
+ }
+ return this;
+ }
+
+ async #run(
+ action: (locator: Locator, signal: AbortSignal) => Promise,
+ signal?: AbortSignal
+ ) {
+ const abortControllers = new WeakMap, AbortController>();
+
+ // Abort all locators if the user-provided signal aborts.
+ signal?.addEventListener('abort', () => {
+ for (const locator of this.#locators) {
+ abortControllers.get(locator)?.abort();
+ }
+ });
+
+ const handleLocatorAction = (locator: Locator): (() => void) => {
+ return () => {
+ // When one locator is ready to act, we will abort other locators.
+ for (const other of this.#locators) {
+ if (other !== locator) {
+ abortControllers.get(other)?.abort();
+ }
+ }
+ this.emit(LocatorEmittedEvents.Action);
+ };
+ };
+
+ const createAbortController = (locator: Locator): AbortController => {
+ const abortController = new AbortController();
+ abortControllers.set(locator, abortController);
+ return abortController;
+ };
+
+ const results = await Promise.allSettled(
+ this.#locators.map(locator => {
+ return action(
+ locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)),
+ createAbortController(locator).signal
+ );
+ })
+ );
+
+ signal?.throwIfAborted();
+
+ const rejected = results.filter(
+ (result): result is PromiseRejectedResult => {
+ return result.status === 'rejected';
+ }
+ );
+
+ // If some locators are fulfilled, do not throw.
+ if (rejected.length !== results.length) {
+ return;
+ }
+
+ for (const result of rejected) {
+ const reason = result.reason;
+ // AbortError is be an expected result of a race.
+ if (isErrorLike(reason) && reason.name === 'AbortError') {
+ continue;
+ }
+ throw reason;
+ }
+ }
+
+ async click(
+ this: RaceLocator,
+ options?: Readonly
+ ): Promise {
+ return await this.#run(
+ (locator, signal) => {
+ return locator.click({...options, signal});
+ },
+ options?.signal
+ );
+ }
+
+ async fill(
+ this: RaceLocator,
+ value: string,
+ options?: Readonly
+ ): Promise {
+ return await this.#run(
+ (locator, signal) => {
+ return locator.fill(value, {...options, signal});
+ },
+ options?.signal
+ );
+ }
+
+ async hover(
+ this: RaceLocator,
+ options?: Readonly
+ ): Promise {
+ return await this.#run(
+ (locator, signal) => {
+ return locator.hover({...options, signal});
+ },
+ options?.signal
+ );
+ }
+
+ async scroll(
+ this: RaceLocator,
+ options?: Readonly
+ ): Promise {
+ return await this.#run(
+ (locator, signal) => {
+ return locator.scroll({...options, signal});
+ },
+ options?.signal
+ );
+ }
+}
diff --git a/packages/puppeteer-core/src/api/locators/locators.ts b/packages/puppeteer-core/src/api/locators/locators.ts
new file mode 100644
index 00000000..fe44cc4c
--- /dev/null
+++ b/packages/puppeteer-core/src/api/locators/locators.ts
@@ -0,0 +1,4 @@
+export * from './Locator.js';
+export * from './NodeLocator.js';
+export * from './ExpectedLocator.js';
+export * from './RaceLocator.js';
diff --git a/test/src/locator.spec.ts b/test/src/locator.spec.ts
index a938128d..89bb437a 100644
--- a/test/src/locator.spec.ts
+++ b/test/src/locator.spec.ts
@@ -19,7 +19,7 @@ import {TimeoutError} from 'puppeteer-core';
import {
Locator,
LocatorEmittedEvents,
-} from 'puppeteer-core/internal/api/Locator.js';
+} from 'puppeteer-core/internal/api/locators/locators.js';
import sinon from 'sinon';
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';