From 47eecf5bb11daba0114ad04282beb01c85eb9405 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:45:10 +0200 Subject: [PATCH] feat: implement `Locator.prototype.map` (#10630) --- docs/api/index.md | 1 + docs/api/puppeteer.locator.map.md | 25 ++++++ docs/api/puppeteer.locator.md | 1 + docs/api/puppeteer.mapper.md | 13 +++ .../src/api/locators/DelegatedLocator.ts | 84 +++++++++++++++++++ .../src/api/locators/ExpectedLocator.ts | 65 ++------------ .../src/api/locators/Locator.ts | 11 +++ .../src/api/locators/MappedLocator.ts | 46 ++++++++++ .../src/api/locators/locators.ts | 2 + test/src/locator.spec.ts | 61 ++++++++++++++ 10 files changed, 252 insertions(+), 57 deletions(-) create mode 100644 docs/api/puppeteer.locator.map.md create mode 100644 docs/api/puppeteer.mapper.md create mode 100644 packages/puppeteer-core/src/api/locators/DelegatedLocator.ts create mode 100644 packages/puppeteer-core/src/api/locators/MappedLocator.ts diff --git a/docs/api/index.md b/docs/api/index.md index 0700b872cee..0d2f5cf4147 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -172,6 +172,7 @@ sidebar_label: API | [KeyPressOptions](./puppeteer.keypressoptions.md) | | | [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | | | [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | | +| [Mapper](./puppeteer.mapper.md) | | | [MouseButton](./puppeteer.mousebutton.md) | | | [NodeFor](./puppeteer.nodefor.md) | | | [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. | diff --git a/docs/api/puppeteer.locator.map.md b/docs/api/puppeteer.locator.map.md new file mode 100644 index 00000000000..7e53a4c85e3 --- /dev/null +++ b/docs/api/puppeteer.locator.map.md @@ -0,0 +1,25 @@ +--- +sidebar_label: Locator.map +--- + +# Locator.map() method + +Maps the locator using the provided mapper. + +#### Signature: + +```typescript +class Locator { + map(mapper: Mapper): Locator; +} +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------------------------------------------- | ----------- | +| mapper | [Mapper](./puppeteer.mapper.md)<T, To> | | + +**Returns:** + +[Locator](./puppeteer.locator.md)<To> diff --git a/docs/api/puppeteer.locator.md b/docs/api/puppeteer.locator.md index e90ddfd50f7..ddd17299e72 100644 --- a/docs/api/puppeteer.locator.md +++ b/docs/api/puppeteer.locator.md @@ -27,6 +27,7 @@ export declare abstract class Locator extends EventEmitter | [click(this, options)](./puppeteer.locator.click.md) | | | | [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. | | [hover(this, options)](./puppeteer.locator.hover.md) | | | +| [map(mapper)](./puppeteer.locator.map.md) | | Maps the locator using the provided mapper. | | [off(eventName, handler)](./puppeteer.locator.off.md) | | | | [on(eventName, handler)](./puppeteer.locator.on.md) | | | | [once(eventName, handler)](./puppeteer.locator.once.md) | | | diff --git a/docs/api/puppeteer.mapper.md b/docs/api/puppeteer.mapper.md new file mode 100644 index 00000000000..ebd2083e741 --- /dev/null +++ b/docs/api/puppeteer.mapper.md @@ -0,0 +1,13 @@ +--- +sidebar_label: Mapper +--- + +# Mapper type + +#### Signature: + +```typescript +export type Mapper = (value: From) => Awaitable; +``` + +**References:** [Awaitable](./puppeteer.awaitable.md) diff --git a/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts b/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts new file mode 100644 index 00000000000..ccd07bb9092 --- /dev/null +++ b/packages/puppeteer-core/src/api/locators/DelegatedLocator.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Observable} from '../../../third_party/rxjs/rxjs.js'; +import {HandleFor} from '../../common/common.js'; + +import {Locator, VisibilityOption} from './locators.js'; + +/** + * @internal + */ +export class DelegatedLocator extends Locator { + #delegate: Locator; + + constructor(delegate: Locator) { + super(); + + this.#delegate = delegate; + this.copyOptions(this.#delegate); + } + + protected get delegate(): Locator { + return this.#delegate; + } + + override setTimeout(timeout: number): this { + super.setTimeout(timeout); + this.#delegate.setTimeout(timeout); + return this; + } + + override setVisibility( + this: DelegatedLocator, + visibility: VisibilityOption + ): Locator { + super.setVisibility(visibility); + this.#delegate.setVisibility(visibility); + return this; + } + + override setWaitForEnabled( + this: DelegatedLocator, + value: boolean + ): Locator { + super.setWaitForEnabled(value); + this.#delegate.setWaitForEnabled(value); + return this; + } + + override setEnsureElementIsInTheViewport( + this: DelegatedLocator, + value: boolean + ): Locator { + super.setEnsureElementIsInTheViewport(value); + this.#delegate.setEnsureElementIsInTheViewport(value); + return this; + } + + override setWaitForStableBoundingBox( + this: DelegatedLocator, + value: boolean + ): Locator { + super.setWaitForStableBoundingBox(value); + this.#delegate.setWaitForStableBoundingBox(value); + return this; + } + + override _wait(): Observable> { + throw new Error('Not implemented'); + } +} diff --git a/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts index b0a8bc6b122..75056c56f8a 100644 --- a/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts +++ b/packages/puppeteer-core/src/api/locators/ExpectedLocator.ts @@ -24,7 +24,8 @@ import { import {Awaitable, HandleFor} from '../../common/common.js'; import {ElementHandle} from '../ElementHandle.js'; -import {ActionOptions, Locator, VisibilityOption} from './locators.js'; +import {DelegatedLocator} from './DelegatedLocator.js'; +import {ActionOptions, Locator} from './locators.js'; /** * @public @@ -36,69 +37,19 @@ export type Predicate = /** * @internal */ -export class ExpectedLocator extends Locator { - #base: Locator; +export class ExpectedLocator extends DelegatedLocator< + From, + To +> { #predicate: Predicate; constructor(base: Locator, predicate: Predicate) { - super(); - - this.#base = base; + super(base); this.#predicate = predicate; - - this.copyOptions(this.#base); - } - - override setTimeout(timeout: number): this { - super.setTimeout(timeout); - this.#base.setTimeout(timeout); - return this; - } - - override setVisibility( - this: ExpectedLocator, - visibility: VisibilityOption - ): Locator { - super.setVisibility(visibility); - this.#base.setVisibility(visibility); - return this; - } - - override setWaitForEnabled( - this: ExpectedLocator, - value: boolean - ): Locator { - super.setWaitForEnabled(value); - this.#base.setWaitForEnabled(value); - return this; - } - - override setEnsureElementIsInTheViewport< - FromElement extends Element, - ToElement extends FromElement, - >( - this: ExpectedLocator, - value: boolean - ): Locator { - super.setEnsureElementIsInTheViewport(value); - this.#base.setEnsureElementIsInTheViewport(value); - return this; - } - - override setWaitForStableBoundingBox< - FromElement extends Element, - ToElement extends FromElement, - >( - this: ExpectedLocator, - value: boolean - ): Locator { - super.setWaitForStableBoundingBox(value); - this.#base.setWaitForStableBoundingBox(value); - return this; } override _wait(options?: Readonly): Observable> { - return this.#base._wait(options).pipe( + return this.delegate._wait(options).pipe( mergeMap(handle => { return from( (handle as ElementHandle).frame.waitForFunction( diff --git a/packages/puppeteer-core/src/api/locators/Locator.ts b/packages/puppeteer-core/src/api/locators/Locator.ts index cbbf2e8737b..b454944ef56 100644 --- a/packages/puppeteer-core/src/api/locators/Locator.ts +++ b/packages/puppeteer-core/src/api/locators/Locator.ts @@ -48,6 +48,8 @@ import { Action, AwaitedLocator, ExpectedLocator, + MappedLocator, + Mapper, Predicate, RaceLocator, } from './locators.js'; @@ -655,6 +657,15 @@ export abstract class Locator extends EventEmitter { } } + /** + * Maps the locator using the provided mapper. + * + * @public + */ + map(mapper: Mapper): Locator { + return new MappedLocator(this, mapper); + } + /** * Creates an expectation that is evaluated against located values. * diff --git a/packages/puppeteer-core/src/api/locators/MappedLocator.ts b/packages/puppeteer-core/src/api/locators/MappedLocator.ts new file mode 100644 index 00000000000..4a3ac91aea3 --- /dev/null +++ b/packages/puppeteer-core/src/api/locators/MappedLocator.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Observable, from, mergeMap} from '../../../third_party/rxjs/rxjs.js'; +import {Awaitable, HandleFor} from '../../common/common.js'; +import {JSHandle} from '../JSHandle.js'; + +import {ActionOptions, DelegatedLocator, Locator} from './locators.js'; + +/** + * @public + */ +export type Mapper = (value: From) => Awaitable; + +/** + * @internal + */ +export class MappedLocator extends DelegatedLocator { + #mapper: Mapper; + + constructor(base: Locator, mapper: Mapper) { + super(base); + this.#mapper = mapper; + } + + override _wait(options?: Readonly): Observable> { + return this.delegate._wait(options).pipe( + mergeMap(handle => { + return from((handle as JSHandle).evaluateHandle(this.#mapper)); + }) + ); + } +} diff --git a/packages/puppeteer-core/src/api/locators/locators.ts b/packages/puppeteer-core/src/api/locators/locators.ts index c717af9914c..77e5fc83403 100644 --- a/packages/puppeteer-core/src/api/locators/locators.ts +++ b/packages/puppeteer-core/src/api/locators/locators.ts @@ -18,3 +18,5 @@ export * from './Locator.js'; export * from './NodeLocator.js'; export * from './ExpectedLocator.js'; export * from './RaceLocator.js'; +export * from './DelegatedLocator.js'; +export * from './MappedLocator.js'; diff --git a/test/src/locator.spec.ts b/test/src/locator.spec.ts index 42c7a77ad5e..7dd7d73f799 100644 --- a/test/src/locator.spec.ts +++ b/test/src/locator.spec.ts @@ -540,6 +540,67 @@ describe('Locator', function () { }); }); + describe('Locator.prototype.map', () => { + it('should work', async () => { + const {page} = await getTestState(); + await page.setContent(`
test
`); + await expect( + page + .locator('::-p-text(test)') + .map(element => { + return element.getAttribute('clickable'); + }) + .wait() + ).resolves.toEqual(null); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect( + page + .locator('::-p-text(test)') + .map(element => { + return element.getAttribute('clickable'); + }) + .wait() + ).resolves.toEqual('true'); + }); + it('should work with throws', async () => { + const {page} = await getTestState(); + await page.setContent(`
test
`); + const result = page + .locator('::-p-text(test)') + .map(element => { + const clickable = element.getAttribute('clickable'); + if (!clickable) { + throw new Error('Missing `clickable` as an attribute'); + } + return clickable; + }) + .wait(); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect(result).resolves.toEqual('true'); + }); + it('should work with expect', async () => { + const {page} = await getTestState(); + await page.setContent(`
test
`); + const result = page + .locator('::-p-text(test)') + .expect(element => { + return element.getAttribute('clickable') !== null; + }) + .map(element => { + return element.getAttribute('clickable'); + }) + .wait(); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect(result).resolves.toEqual('true'); + }); + }); + describe('Locator.prototype.expect', () => { it('should resolve as soon as the predicate matches', async () => { const clock = sinon.useFakeTimers({