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>
|
@ -17,14 +17,16 @@ export declare abstract class Locator<T> extends EventEmitter
|
|||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
| Property | Modifiers | Type | Description |
|
| Property | Modifiers | Type | Description |
|
||||||
| -------- | --------------------- | ---- | ------------------------------------------------------------ |
|
| -------- | --------------------- | ------ | ------------------------------------------------------------ |
|
||||||
| \_ | <code>optional</code> | T | Used for nominally typing [Locator](./puppeteer.locator.md). |
|
| \_ | <code>optional</code> | T | Used for nominally typing [Locator](./puppeteer.locator.md). |
|
||||||
|
| timeout | <code>readonly</code> | number | |
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
| Method | Modifiers | Description |
|
| Method | Modifiers | Description |
|
||||||
| ------------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [click(this, options)](./puppeteer.locator.click.md) | | |
|
| [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. |
|
| [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> |
|
| [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) | | |
|
| [hover(this, options)](./puppeteer.locator.hover.md) | | |
|
||||||
|
@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class Locator {
|
class Locator {
|
||||||
setTimeout(timeout: number): this;
|
setTimeout(timeout: number): Locator<T>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -20,4 +20,4 @@ class Locator {
|
|||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
this
|
[Locator](./puppeteer.locator.md)<T>
|
||||||
|
@ -22,7 +22,7 @@ import {Locator, VisibilityOption} from './locators.js';
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class DelegatedLocator<T, U> extends Locator<U> {
|
export abstract class DelegatedLocator<T, U> extends Locator<U> {
|
||||||
#delegate: Locator<T>;
|
#delegate: Locator<T>;
|
||||||
|
|
||||||
constructor(delegate: Locator<T>) {
|
constructor(delegate: Locator<T>) {
|
||||||
@ -36,49 +36,62 @@ export class DelegatedLocator<T, U> extends Locator<U> {
|
|||||||
return this.#delegate;
|
return this.#delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setTimeout(timeout: number): this {
|
override setTimeout(timeout: number): DelegatedLocator<T, U> {
|
||||||
super.setTimeout(timeout);
|
const locator = super.setTimeout(timeout) as DelegatedLocator<T, U>;
|
||||||
this.#delegate.setTimeout(timeout);
|
locator.#delegate = this.#delegate.setTimeout(timeout);
|
||||||
return this;
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setVisibility<T extends Node, U extends T>(
|
override setVisibility<ValueType extends Node, NodeType extends Node>(
|
||||||
this: DelegatedLocator<T, U>,
|
this: DelegatedLocator<ValueType, NodeType>,
|
||||||
visibility: VisibilityOption
|
visibility: VisibilityOption
|
||||||
): Locator<U> {
|
): DelegatedLocator<ValueType, NodeType> {
|
||||||
super.setVisibility(visibility);
|
const locator = super.setVisibility<NodeType>(
|
||||||
this.#delegate.setVisibility(visibility);
|
visibility
|
||||||
return this;
|
) as DelegatedLocator<ValueType, NodeType>;
|
||||||
|
locator.#delegate = locator.#delegate.setVisibility<ValueType>(visibility);
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setWaitForEnabled<T extends Node, U extends T>(
|
override setWaitForEnabled<ValueType extends Node, NodeType extends Node>(
|
||||||
this: DelegatedLocator<T, U>,
|
this: DelegatedLocator<ValueType, NodeType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<U> {
|
): DelegatedLocator<ValueType, NodeType> {
|
||||||
super.setWaitForEnabled(value);
|
const locator = super.setWaitForEnabled<NodeType>(
|
||||||
this.#delegate.setWaitForEnabled(value);
|
value
|
||||||
return this;
|
) as DelegatedLocator<ValueType, NodeType>;
|
||||||
|
locator.#delegate = this.#delegate.setWaitForEnabled(value);
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setEnsureElementIsInTheViewport<T extends Element, U extends T>(
|
override setEnsureElementIsInTheViewport<
|
||||||
this: DelegatedLocator<T, U>,
|
ValueType extends Element,
|
||||||
|
ElementType extends Element,
|
||||||
|
>(
|
||||||
|
this: DelegatedLocator<ValueType, ElementType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<U> {
|
): DelegatedLocator<ValueType, ElementType> {
|
||||||
super.setEnsureElementIsInTheViewport(value);
|
const locator = super.setEnsureElementIsInTheViewport<ElementType>(
|
||||||
this.#delegate.setEnsureElementIsInTheViewport(value);
|
value
|
||||||
return this;
|
) as DelegatedLocator<ValueType, ElementType>;
|
||||||
|
locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setWaitForStableBoundingBox<T extends Element, U extends T>(
|
override setWaitForStableBoundingBox<
|
||||||
this: DelegatedLocator<T, U>,
|
ValueType extends Element,
|
||||||
|
ElementType extends Element,
|
||||||
|
>(
|
||||||
|
this: DelegatedLocator<ValueType, ElementType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<U> {
|
): DelegatedLocator<ValueType, ElementType> {
|
||||||
super.setWaitForStableBoundingBox(value);
|
const locator = super.setWaitForStableBoundingBox<ElementType>(
|
||||||
this.#delegate.setWaitForStableBoundingBox(value);
|
value
|
||||||
return this;
|
) as DelegatedLocator<ValueType, ElementType>;
|
||||||
|
locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
override _wait(): Observable<HandleFor<U>> {
|
abstract override _clone(): DelegatedLocator<T, U>;
|
||||||
throw new Error('Not implemented');
|
abstract override _wait(): Observable<HandleFor<U>>;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -48,13 +48,20 @@ export class FilteredLocator<From, To extends From> extends DelegatedLocator<
|
|||||||
this.#predicate = predicate;
|
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>> {
|
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
|
||||||
return this.delegate._wait(options).pipe(
|
return this.delegate._wait(options).pipe(
|
||||||
mergeMap(handle => {
|
mergeMap(handle => {
|
||||||
return from(
|
return from(
|
||||||
(handle as ElementHandle<Node>).frame.waitForFunction(
|
(handle as ElementHandle<Node>).frame.waitForFunction(
|
||||||
this.#predicate,
|
this.#predicate,
|
||||||
{signal: options?.signal, timeout: this.timeout},
|
{signal: options?.signal, timeout: this._timeout},
|
||||||
handle
|
handle
|
||||||
)
|
)
|
||||||
).pipe(
|
).pipe(
|
||||||
|
@ -178,7 +178,7 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected timeout = 30_000;
|
protected _timeout = 30_000;
|
||||||
#ensureElementIsInTheViewport = true;
|
#ensureElementIsInTheViewport = true;
|
||||||
#waitForEnabled = true;
|
#waitForEnabled = true;
|
||||||
#waitForStableBoundingBox = true;
|
#waitForStableBoundingBox = true;
|
||||||
@ -212,12 +212,12 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.timeout > 0) {
|
if (this._timeout > 0) {
|
||||||
candidates.push(
|
candidates.push(
|
||||||
timer(this.timeout).pipe(
|
timer(this._timeout).pipe(
|
||||||
map(() => {
|
map(() => {
|
||||||
throw new TimeoutError(
|
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>(
|
override on<K extends keyof LocatorEventObject>(
|
||||||
eventName: K,
|
eventName: K,
|
||||||
handler: (event: LocatorEventObject[K]) => void
|
handler: (event: LocatorEventObject[K]) => void
|
||||||
@ -251,48 +256,53 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
return super.off(eventName, handler);
|
return super.off(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(timeout: number): this {
|
setTimeout(timeout: number): Locator<T> {
|
||||||
this.timeout = timeout;
|
const locator = this._clone();
|
||||||
return this;
|
locator._timeout = timeout;
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
setVisibility<NodeType extends Node>(
|
setVisibility<NodeType extends Node>(
|
||||||
this: Locator<NodeType>,
|
this: Locator<NodeType>,
|
||||||
visibility: VisibilityOption
|
visibility: VisibilityOption
|
||||||
): Locator<NodeType> {
|
): Locator<NodeType> {
|
||||||
this.visibility = visibility;
|
const locator = this._clone();
|
||||||
return this;
|
locator.visibility = visibility;
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWaitForEnabled<NodeType extends Node>(
|
setWaitForEnabled<NodeType extends Node>(
|
||||||
this: Locator<NodeType>,
|
this: Locator<NodeType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<NodeType> {
|
): Locator<NodeType> {
|
||||||
this.#waitForEnabled = value;
|
const locator = this._clone();
|
||||||
return this;
|
locator.#waitForEnabled = value;
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
||||||
this: Locator<ElementType>,
|
this: Locator<ElementType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<ElementType> {
|
): Locator<ElementType> {
|
||||||
this.#ensureElementIsInTheViewport = value;
|
const locator = this._clone();
|
||||||
return this;
|
locator.#ensureElementIsInTheViewport = value;
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWaitForStableBoundingBox<ElementType extends Element>(
|
setWaitForStableBoundingBox<ElementType extends Element>(
|
||||||
this: Locator<ElementType>,
|
this: Locator<ElementType>,
|
||||||
value: boolean
|
value: boolean
|
||||||
): Locator<ElementType> {
|
): Locator<ElementType> {
|
||||||
this.#waitForStableBoundingBox = value;
|
const locator = this._clone();
|
||||||
return this;
|
locator.#waitForStableBoundingBox = value;
|
||||||
|
return locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
copyOptions(locator: Locator<any>): this {
|
copyOptions<T>(locator: Locator<T>): this {
|
||||||
this.timeout = locator.timeout;
|
this._timeout = locator._timeout;
|
||||||
this.visibility = locator.visibility;
|
this.visibility = locator.visibility;
|
||||||
this.#waitForEnabled = locator.#waitForEnabled;
|
this.#waitForEnabled = locator.#waitForEnabled;
|
||||||
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
|
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
|
||||||
@ -320,7 +330,7 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeout: this.timeout,
|
timeout: this._timeout,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
handle
|
handle
|
||||||
@ -632,11 +642,23 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
abstract _clone(): Locator<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
abstract _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>>;
|
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.
|
* Waits for the locator to get the serialized value from the page.
|
||||||
*
|
*
|
||||||
@ -663,7 +685,7 @@ export abstract class Locator<T> extends EventEmitter {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
map<To>(mapper: Mapper<T, To>): Locator<To> {
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
|
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>(
|
click<ElementType extends Element>(
|
||||||
|
@ -36,6 +36,12 @@ export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
|
|||||||
this.#mapper = mapper;
|
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>> {
|
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
|
||||||
return this.delegate._wait(options).pipe(
|
return this.delegate._wait(options).pipe(
|
||||||
mergeMap(handle => {
|
mergeMap(handle => {
|
||||||
|
@ -90,13 +90,19 @@ export class NodeLocator<T extends Node> extends Locator<T> {
|
|||||||
})().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
|
})().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;
|
const signal = options?.signal;
|
||||||
return defer(() => {
|
return defer(() => {
|
||||||
return from(
|
return from(
|
||||||
this.#pageOrFrame.waitForSelector(this.#selector, {
|
this.#pageOrFrame.waitForSelector(this.#selector, {
|
||||||
visible: false,
|
visible: false,
|
||||||
timeout: this.timeout,
|
timeout: this._timeout,
|
||||||
signal,
|
signal,
|
||||||
}) as Promise<HandleFor<T> | null>
|
}) as Promise<HandleFor<T> | null>
|
||||||
);
|
);
|
||||||
|
@ -53,6 +53,14 @@ export class RaceLocator<T> extends Locator<T> {
|
|||||||
this.#locators = locators;
|
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>> {
|
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
|
||||||
return race(
|
return race(
|
||||||
...this.#locators.map(locator => {
|
...this.#locators.map(locator => {
|
||||||
|
@ -647,4 +647,29 @@ describe('Locator', function () {
|
|||||||
await page.locator('div').wait();
|
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