diff --git a/docs/api/puppeteer.locator.clone.md b/docs/api/puppeteer.locator.clone.md new file mode 100644 index 00000000..fa585d96 --- /dev/null +++ b/docs/api/puppeteer.locator.clone.md @@ -0,0 +1,19 @@ +--- +sidebar_label: Locator.clone +--- + +# Locator.clone() method + +Clones the locator. + +#### Signature: + +```typescript +class Locator { + clone(): Locator; +} +``` + +**Returns:** + +[Locator](./puppeteer.locator.md)<T> diff --git a/docs/api/puppeteer.locator.md b/docs/api/puppeteer.locator.md index 0eb49ab7..86a6312b 100644 --- a/docs/api/puppeteer.locator.md +++ b/docs/api/puppeteer.locator.md @@ -16,15 +16,17 @@ export declare abstract class Locator extends EventEmitter ## Properties -| Property | Modifiers | Type | Description | -| -------- | --------------------- | ---- | ------------------------------------------------------------ | -| \_ | optional | T | Used for nominally typing [Locator](./puppeteer.locator.md). | +| Property | Modifiers | Type | Description | +| -------- | --------------------- | ------ | ------------------------------------------------------------ | +| \_ | optional | T | Used for nominally typing [Locator](./puppeteer.locator.md). | +| timeout | readonly | 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) | |

Creates an expectation that is evaluated against located values.

If the expectations do not match, then the locator will retry.

| | [hover(this, options)](./puppeteer.locator.hover.md) | | | diff --git a/docs/api/puppeteer.locator.settimeout.md b/docs/api/puppeteer.locator.settimeout.md index 33099941..24c08446 100644 --- a/docs/api/puppeteer.locator.settimeout.md +++ b/docs/api/puppeteer.locator.settimeout.md @@ -8,7 +8,7 @@ sidebar_label: Locator.setTimeout ```typescript class Locator { - setTimeout(timeout: number): this; + setTimeout(timeout: number): Locator; } ``` @@ -20,4 +20,4 @@ class Locator { **Returns:** -this +[Locator](./puppeteer.locator.md)<T> diff --git a/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts b/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts index ccd07bb9..53d95264 100644 --- a/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts +++ b/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts @@ -22,7 +22,7 @@ import {Locator, VisibilityOption} from './locators.js'; /** * @internal */ -export class DelegatedLocator extends Locator { +export abstract class DelegatedLocator extends Locator { #delegate: Locator; constructor(delegate: Locator) { @@ -36,49 +36,62 @@ export class DelegatedLocator extends Locator { return this.#delegate; } - override setTimeout(timeout: number): this { - super.setTimeout(timeout); - this.#delegate.setTimeout(timeout); - return this; + override setTimeout(timeout: number): DelegatedLocator { + const locator = super.setTimeout(timeout) as DelegatedLocator; + locator.#delegate = this.#delegate.setTimeout(timeout); + return locator; } - override setVisibility( - this: DelegatedLocator, + override setVisibility( + this: DelegatedLocator, visibility: VisibilityOption - ): Locator { - super.setVisibility(visibility); - this.#delegate.setVisibility(visibility); - return this; + ): DelegatedLocator { + const locator = super.setVisibility( + visibility + ) as DelegatedLocator; + locator.#delegate = locator.#delegate.setVisibility(visibility); + return locator; } - override setWaitForEnabled( - this: DelegatedLocator, + override setWaitForEnabled( + this: DelegatedLocator, value: boolean - ): Locator { - super.setWaitForEnabled(value); - this.#delegate.setWaitForEnabled(value); - return this; + ): DelegatedLocator { + const locator = super.setWaitForEnabled( + value + ) as DelegatedLocator; + locator.#delegate = this.#delegate.setWaitForEnabled(value); + return locator; } - override setEnsureElementIsInTheViewport( - this: DelegatedLocator, + override setEnsureElementIsInTheViewport< + ValueType extends Element, + ElementType extends Element, + >( + this: DelegatedLocator, value: boolean - ): Locator { - super.setEnsureElementIsInTheViewport(value); - this.#delegate.setEnsureElementIsInTheViewport(value); - return this; + ): DelegatedLocator { + const locator = super.setEnsureElementIsInTheViewport( + value + ) as DelegatedLocator; + locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value); + return locator; } - override setWaitForStableBoundingBox( - this: DelegatedLocator, + override setWaitForStableBoundingBox< + ValueType extends Element, + ElementType extends Element, + >( + this: DelegatedLocator, value: boolean - ): Locator { - super.setWaitForStableBoundingBox(value); - this.#delegate.setWaitForStableBoundingBox(value); - return this; + ): DelegatedLocator { + const locator = super.setWaitForStableBoundingBox( + value + ) as DelegatedLocator; + locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value); + return locator; } - override _wait(): Observable> { - throw new Error('Not implemented'); - } + abstract override _clone(): DelegatedLocator; + abstract override _wait(): Observable>; } diff --git a/packages/puppeteer-core/src/api/locators/FilteredLocator.ts b/packages/puppeteer-core/src/api/locators/FilteredLocator.ts index b9d9ebef..ca2a9da5 100644 --- a/packages/puppeteer-core/src/api/locators/FilteredLocator.ts +++ b/packages/puppeteer-core/src/api/locators/FilteredLocator.ts @@ -48,13 +48,20 @@ export class FilteredLocator extends DelegatedLocator< this.#predicate = predicate; } + override _clone(): FilteredLocator { + return new FilteredLocator( + this.delegate.clone(), + this.#predicate + ).copyOptions(this); + } + override _wait(options?: Readonly): Observable> { return this.delegate._wait(options).pipe( mergeMap(handle => { return from( (handle as ElementHandle).frame.waitForFunction( this.#predicate, - {signal: options?.signal, timeout: this.timeout}, + {signal: options?.signal, timeout: this._timeout}, handle ) ).pipe( diff --git a/packages/puppeteer-core/src/api/locators/Locator.ts b/packages/puppeteer-core/src/api/locators/Locator.ts index c65901f9..6e710675 100644 --- a/packages/puppeteer-core/src/api/locators/Locator.ts +++ b/packages/puppeteer-core/src/api/locators/Locator.ts @@ -178,7 +178,7 @@ export abstract class Locator 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 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 extends EventEmitter { }, }; + // Determines when the locator will timeout for actions. + get timeout(): number { + return this._timeout; + } + override on( eventName: K, handler: (event: LocatorEventObject[K]) => void @@ -251,48 +256,53 @@ export abstract class Locator extends EventEmitter { return super.off(eventName, handler); } - setTimeout(timeout: number): this { - this.timeout = timeout; - return this; + setTimeout(timeout: number): Locator { + const locator = this._clone(); + locator._timeout = timeout; + return locator; } setVisibility( this: Locator, visibility: VisibilityOption ): Locator { - this.visibility = visibility; - return this; + const locator = this._clone(); + locator.visibility = visibility; + return locator; } setWaitForEnabled( this: Locator, value: boolean ): Locator { - this.#waitForEnabled = value; - return this; + const locator = this._clone(); + locator.#waitForEnabled = value; + return locator; } setEnsureElementIsInTheViewport( this: Locator, value: boolean ): Locator { - this.#ensureElementIsInTheViewport = value; - return this; + const locator = this._clone(); + locator.#ensureElementIsInTheViewport = value; + return locator; } setWaitForStableBoundingBox( this: Locator, value: boolean ): Locator { - this.#waitForStableBoundingBox = value; - return this; + const locator = this._clone(); + locator.#waitForStableBoundingBox = value; + return locator; } /** * @internal */ - copyOptions(locator: Locator): this { - this.timeout = locator.timeout; + copyOptions(locator: Locator): 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 extends EventEmitter { return true; }, { - timeout: this.timeout, + timeout: this._timeout, signal, }, handle @@ -632,11 +642,23 @@ export abstract class Locator extends EventEmitter { ); } + /** + * @internal + */ + abstract _clone(): Locator; + /** * @internal */ abstract _wait(options?: Readonly): Observable>; + /** + * Clones the locator. + */ + clone(): Locator { + return this._clone(); + } + /** * Waits for the locator to get the serialized value from the page. * @@ -663,7 +685,7 @@ export abstract class Locator extends EventEmitter { * @public */ map(mapper: Mapper): Locator { - return new MappedLocator(this, mapper); + return new MappedLocator(this._clone(), mapper); } /** @@ -674,7 +696,7 @@ export abstract class Locator extends EventEmitter { * @public */ filter(predicate: Predicate): Locator { - return new FilteredLocator(this, predicate); + return new FilteredLocator(this._clone(), predicate); } click( diff --git a/packages/puppeteer-core/src/api/locators/MappedLocator.ts b/packages/puppeteer-core/src/api/locators/MappedLocator.ts index 4a3ac91a..4d71a9ad 100644 --- a/packages/puppeteer-core/src/api/locators/MappedLocator.ts +++ b/packages/puppeteer-core/src/api/locators/MappedLocator.ts @@ -36,6 +36,12 @@ export class MappedLocator extends DelegatedLocator { this.#mapper = mapper; } + override _clone(): MappedLocator { + return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions( + this + ); + } + override _wait(options?: Readonly): Observable> { return this.delegate._wait(options).pipe( mergeMap(handle => { diff --git a/packages/puppeteer-core/src/api/locators/NodeLocator.ts b/packages/puppeteer-core/src/api/locators/NodeLocator.ts index c70ada13..ca98569f 100644 --- a/packages/puppeteer-core/src/api/locators/NodeLocator.ts +++ b/packages/puppeteer-core/src/api/locators/NodeLocator.ts @@ -90,13 +90,19 @@ export class NodeLocator extends Locator { })().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements()); }; - _wait(options?: Readonly): Observable> { + override _clone(): NodeLocator { + return new NodeLocator(this.#pageOrFrame, this.#selector).copyOptions( + this + ); + } + + override _wait(options?: Readonly): Observable> { const signal = options?.signal; return defer(() => { return from( this.#pageOrFrame.waitForSelector(this.#selector, { visible: false, - timeout: this.timeout, + timeout: this._timeout, signal, }) as Promise | null> ); diff --git a/packages/puppeteer-core/src/api/locators/RaceLocator.ts b/packages/puppeteer-core/src/api/locators/RaceLocator.ts index e5890d42..e0d5c223 100644 --- a/packages/puppeteer-core/src/api/locators/RaceLocator.ts +++ b/packages/puppeteer-core/src/api/locators/RaceLocator.ts @@ -53,6 +53,14 @@ export class RaceLocator extends Locator { this.#locators = locators; } + override _clone(): RaceLocator { + return new RaceLocator( + this.#locators.map(locator => { + return locator.clone(); + }) + ).copyOptions(this); + } + override _wait(options?: Readonly): Observable> { return race( ...this.#locators.map(locator => { diff --git a/test/src/locator.spec.ts b/test/src/locator.spec.ts index ef5b9c45..d6511c1f 100644 --- a/test/src/locator.spec.ts +++ b/test/src/locator.spec.ts @@ -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); + } + }); + }); });