feat: implement immutable locator operations (#10638)

This commit is contained in:
jrandolf 2023-07-26 17:00:00 +02:00 committed by GitHub
parent 30ccbf855a
commit 34be28db5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 168 additions and 60 deletions

View 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)&lt;T&gt;

View File

@ -17,14 +17,16 @@ 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). |
| 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) | | |

View File

@ -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)&lt;T&gt;

View File

@ -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>>;
}

View File

@ -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(

View File

@ -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>(

View File

@ -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 => {

View File

@ -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>
);

View File

@ -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 => {

View File

@ -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);
}
});
});
});