feat: implement immutable locator operations (#10638)
This commit is contained in:
parent
30ccbf855a
commit
34be28db5d
19
docs/api/puppeteer.locator.clone.md
Normal file
19
docs/api/puppeteer.locator.clone.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
sidebar_label: Locator.clone
|
||||
---
|
||||
|
||||
# Locator.clone() method
|
||||
|
||||
Clones the locator.
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
clone(): Locator<T>;
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
|
||||
[Locator](./puppeteer.locator.md)<T>
|
@ -16,15 +16,17 @@ export declare abstract class Locator<T> extends EventEmitter
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| -------- | --------------------- | ---- | ------------------------------------------------------------ |
|
||||
| \_ | <code>optional</code> | T | Used for nominally typing [Locator](./puppeteer.locator.md). |
|
||||
| Property | Modifiers | Type | Description |
|
||||
| -------- | --------------------- | ------ | ------------------------------------------------------------ |
|
||||
| \_ | <code>optional</code> | T | Used for nominally typing [Locator](./puppeteer.locator.md). |
|
||||
| timeout | <code>readonly</code> | number | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| ------------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [click(this, options)](./puppeteer.locator.click.md) | | |
|
||||
| [clone()](./puppeteer.locator.clone.md) | | Clones the locator. |
|
||||
| [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. |
|
||||
| [filter(predicate)](./puppeteer.locator.filter.md) | | <p>Creates an expectation that is evaluated against located values.</p><p>If the expectations do not match, then the locator will retry.</p> |
|
||||
| [hover(this, options)](./puppeteer.locator.hover.md) | | |
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
|
||||
|
||||
```typescript
|
||||
class Locator {
|
||||
setTimeout(timeout: number): this;
|
||||
setTimeout(timeout: number): Locator<T>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -20,4 +20,4 @@ class Locator {
|
||||
|
||||
**Returns:**
|
||||
|
||||
this
|
||||
[Locator](./puppeteer.locator.md)<T>
|
||||
|
@ -22,7 +22,7 @@ import {Locator, VisibilityOption} from './locators.js';
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class DelegatedLocator<T, U> extends Locator<U> {
|
||||
export abstract class DelegatedLocator<T, U> extends Locator<U> {
|
||||
#delegate: Locator<T>;
|
||||
|
||||
constructor(delegate: Locator<T>) {
|
||||
@ -36,49 +36,62 @@ export class DelegatedLocator<T, U> extends Locator<U> {
|
||||
return this.#delegate;
|
||||
}
|
||||
|
||||
override setTimeout(timeout: number): this {
|
||||
super.setTimeout(timeout);
|
||||
this.#delegate.setTimeout(timeout);
|
||||
return this;
|
||||
override setTimeout(timeout: number): DelegatedLocator<T, U> {
|
||||
const locator = super.setTimeout(timeout) as DelegatedLocator<T, U>;
|
||||
locator.#delegate = this.#delegate.setTimeout(timeout);
|
||||
return locator;
|
||||
}
|
||||
|
||||
override setVisibility<T extends Node, U extends T>(
|
||||
this: DelegatedLocator<T, U>,
|
||||
override setVisibility<ValueType extends Node, NodeType extends Node>(
|
||||
this: DelegatedLocator<ValueType, NodeType>,
|
||||
visibility: VisibilityOption
|
||||
): Locator<U> {
|
||||
super.setVisibility(visibility);
|
||||
this.#delegate.setVisibility(visibility);
|
||||
return this;
|
||||
): DelegatedLocator<ValueType, NodeType> {
|
||||
const locator = super.setVisibility<NodeType>(
|
||||
visibility
|
||||
) as DelegatedLocator<ValueType, NodeType>;
|
||||
locator.#delegate = locator.#delegate.setVisibility<ValueType>(visibility);
|
||||
return locator;
|
||||
}
|
||||
|
||||
override setWaitForEnabled<T extends Node, U extends T>(
|
||||
this: DelegatedLocator<T, U>,
|
||||
override setWaitForEnabled<ValueType extends Node, NodeType extends Node>(
|
||||
this: DelegatedLocator<ValueType, NodeType>,
|
||||
value: boolean
|
||||
): Locator<U> {
|
||||
super.setWaitForEnabled(value);
|
||||
this.#delegate.setWaitForEnabled(value);
|
||||
return this;
|
||||
): DelegatedLocator<ValueType, NodeType> {
|
||||
const locator = super.setWaitForEnabled<NodeType>(
|
||||
value
|
||||
) as DelegatedLocator<ValueType, NodeType>;
|
||||
locator.#delegate = this.#delegate.setWaitForEnabled(value);
|
||||
return locator;
|
||||
}
|
||||
|
||||
override setEnsureElementIsInTheViewport<T extends Element, U extends T>(
|
||||
this: DelegatedLocator<T, U>,
|
||||
override setEnsureElementIsInTheViewport<
|
||||
ValueType extends Element,
|
||||
ElementType extends Element,
|
||||
>(
|
||||
this: DelegatedLocator<ValueType, ElementType>,
|
||||
value: boolean
|
||||
): Locator<U> {
|
||||
super.setEnsureElementIsInTheViewport(value);
|
||||
this.#delegate.setEnsureElementIsInTheViewport(value);
|
||||
return this;
|
||||
): DelegatedLocator<ValueType, ElementType> {
|
||||
const locator = super.setEnsureElementIsInTheViewport<ElementType>(
|
||||
value
|
||||
) as DelegatedLocator<ValueType, ElementType>;
|
||||
locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
|
||||
return locator;
|
||||
}
|
||||
|
||||
override setWaitForStableBoundingBox<T extends Element, U extends T>(
|
||||
this: DelegatedLocator<T, U>,
|
||||
override setWaitForStableBoundingBox<
|
||||
ValueType extends Element,
|
||||
ElementType extends Element,
|
||||
>(
|
||||
this: DelegatedLocator<ValueType, ElementType>,
|
||||
value: boolean
|
||||
): Locator<U> {
|
||||
super.setWaitForStableBoundingBox(value);
|
||||
this.#delegate.setWaitForStableBoundingBox(value);
|
||||
return this;
|
||||
): DelegatedLocator<ValueType, ElementType> {
|
||||
const locator = super.setWaitForStableBoundingBox<ElementType>(
|
||||
value
|
||||
) as DelegatedLocator<ValueType, ElementType>;
|
||||
locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
|
||||
return locator;
|
||||
}
|
||||
|
||||
override _wait(): Observable<HandleFor<U>> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
abstract override _clone(): DelegatedLocator<T, U>;
|
||||
abstract override _wait(): Observable<HandleFor<U>>;
|
||||
}
|
||||
|
@ -48,13 +48,20 @@ export class FilteredLocator<From, To extends From> extends DelegatedLocator<
|
||||
this.#predicate = predicate;
|
||||
}
|
||||
|
||||
override _clone(): FilteredLocator<From, To> {
|
||||
return new FilteredLocator(
|
||||
this.delegate.clone(),
|
||||
this.#predicate
|
||||
).copyOptions(this);
|
||||
}
|
||||
|
||||
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
|
||||
return this.delegate._wait(options).pipe(
|
||||
mergeMap(handle => {
|
||||
return from(
|
||||
(handle as ElementHandle<Node>).frame.waitForFunction(
|
||||
this.#predicate,
|
||||
{signal: options?.signal, timeout: this.timeout},
|
||||
{signal: options?.signal, timeout: this._timeout},
|
||||
handle
|
||||
)
|
||||
).pipe(
|
||||
|
@ -178,7 +178,7 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected timeout = 30_000;
|
||||
protected _timeout = 30_000;
|
||||
#ensureElementIsInTheViewport = true;
|
||||
#waitForEnabled = true;
|
||||
#waitForStableBoundingBox = true;
|
||||
@ -212,12 +212,12 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
)
|
||||
);
|
||||
}
|
||||
if (this.timeout > 0) {
|
||||
if (this._timeout > 0) {
|
||||
candidates.push(
|
||||
timer(this.timeout).pipe(
|
||||
timer(this._timeout).pipe(
|
||||
map(() => {
|
||||
throw new TimeoutError(
|
||||
`Timed out after waiting ${this.timeout}ms`
|
||||
`Timed out after waiting ${this._timeout}ms`
|
||||
);
|
||||
})
|
||||
)
|
||||
@ -230,6 +230,11 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
},
|
||||
};
|
||||
|
||||
// Determines when the locator will timeout for actions.
|
||||
get timeout(): number {
|
||||
return this._timeout;
|
||||
}
|
||||
|
||||
override on<K extends keyof LocatorEventObject>(
|
||||
eventName: K,
|
||||
handler: (event: LocatorEventObject[K]) => void
|
||||
@ -251,48 +256,53 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
return super.off(eventName, handler);
|
||||
}
|
||||
|
||||
setTimeout(timeout: number): this {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
setTimeout(timeout: number): Locator<T> {
|
||||
const locator = this._clone();
|
||||
locator._timeout = timeout;
|
||||
return locator;
|
||||
}
|
||||
|
||||
setVisibility<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
visibility: VisibilityOption
|
||||
): Locator<NodeType> {
|
||||
this.visibility = visibility;
|
||||
return this;
|
||||
const locator = this._clone();
|
||||
locator.visibility = visibility;
|
||||
return locator;
|
||||
}
|
||||
|
||||
setWaitForEnabled<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
value: boolean
|
||||
): Locator<NodeType> {
|
||||
this.#waitForEnabled = value;
|
||||
return this;
|
||||
const locator = this._clone();
|
||||
locator.#waitForEnabled = value;
|
||||
return locator;
|
||||
}
|
||||
|
||||
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType> {
|
||||
this.#ensureElementIsInTheViewport = value;
|
||||
return this;
|
||||
const locator = this._clone();
|
||||
locator.#ensureElementIsInTheViewport = value;
|
||||
return locator;
|
||||
}
|
||||
|
||||
setWaitForStableBoundingBox<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
): Locator<ElementType> {
|
||||
this.#waitForStableBoundingBox = value;
|
||||
return this;
|
||||
const locator = this._clone();
|
||||
locator.#waitForStableBoundingBox = value;
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
copyOptions(locator: Locator<any>): this {
|
||||
this.timeout = locator.timeout;
|
||||
copyOptions<T>(locator: Locator<T>): this {
|
||||
this._timeout = locator._timeout;
|
||||
this.visibility = locator.visibility;
|
||||
this.#waitForEnabled = locator.#waitForEnabled;
|
||||
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
|
||||
@ -320,7 +330,7 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
timeout: this.timeout,
|
||||
timeout: this._timeout,
|
||||
signal,
|
||||
},
|
||||
handle
|
||||
@ -632,11 +642,23 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _clone(): Locator<T>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>>;
|
||||
|
||||
/**
|
||||
* Clones the locator.
|
||||
*/
|
||||
clone(): Locator<T> {
|
||||
return this._clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the locator to get the serialized value from the page.
|
||||
*
|
||||
@ -663,7 +685,7 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
* @public
|
||||
*/
|
||||
map<To>(mapper: Mapper<T, To>): Locator<To> {
|
||||
return new MappedLocator(this, mapper);
|
||||
return new MappedLocator(this._clone(), mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -674,7 +696,7 @@ export abstract class Locator<T> extends EventEmitter {
|
||||
* @public
|
||||
*/
|
||||
filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
|
||||
return new FilteredLocator(this, predicate);
|
||||
return new FilteredLocator(this._clone(), predicate);
|
||||
}
|
||||
|
||||
click<ElementType extends Element>(
|
||||
|
@ -36,6 +36,12 @@ export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
|
||||
this.#mapper = mapper;
|
||||
}
|
||||
|
||||
override _clone(): MappedLocator<From, To> {
|
||||
return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
|
||||
return this.delegate._wait(options).pipe(
|
||||
mergeMap(handle => {
|
||||
|
@ -90,13 +90,19 @@ export class NodeLocator<T extends Node> extends Locator<T> {
|
||||
})().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
|
||||
};
|
||||
|
||||
_wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
|
||||
override _clone(): NodeLocator<T> {
|
||||
return new NodeLocator<T>(this.#pageOrFrame, this.#selector).copyOptions(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
override _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,
|
||||
timeout: this._timeout,
|
||||
signal,
|
||||
}) as Promise<HandleFor<T> | null>
|
||||
);
|
||||
|
@ -53,6 +53,14 @@ export class RaceLocator<T> extends Locator<T> {
|
||||
this.#locators = locators;
|
||||
}
|
||||
|
||||
override _clone(): RaceLocator<T> {
|
||||
return new RaceLocator<T>(
|
||||
this.#locators.map(locator => {
|
||||
return locator.clone();
|
||||
})
|
||||
).copyOptions(this);
|
||||
}
|
||||
|
||||
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
|
||||
return race(
|
||||
...this.#locators.map(locator => {
|
||||
|
@ -647,4 +647,29 @@ describe('Locator', function () {
|
||||
await page.locator('div').wait();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Locator.prototype.clone', () => {
|
||||
it('should work', async () => {
|
||||
const {page} = await getTestState();
|
||||
const locator = page.locator('div');
|
||||
const clone = locator.clone();
|
||||
expect(locator).not.toStrictEqual(clone);
|
||||
});
|
||||
it('should work internally with delegated locators', async () => {
|
||||
const {page} = await getTestState();
|
||||
const locator = page.locator('div');
|
||||
const delegatedLocators = [
|
||||
locator.map(div => {
|
||||
return div.textContent;
|
||||
}),
|
||||
locator.filter(div => {
|
||||
return div.textContent?.length === 0;
|
||||
}),
|
||||
];
|
||||
for (let delegatedLocator of delegatedLocators) {
|
||||
delegatedLocator = delegatedLocator.setTimeout(500);
|
||||
expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user