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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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