feat: implement Locator.prototype.map (#10630)

This commit is contained in:
jrandolf 2023-07-25 16:45:10 +02:00 committed by GitHub
parent 5d34d42d15
commit 47eecf5bb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 252 additions and 57 deletions

View File

@ -172,6 +172,7 @@ sidebar_label: API
| [KeyPressOptions](./puppeteer.keypressoptions.md) | | | [KeyPressOptions](./puppeteer.keypressoptions.md) | |
| [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | | | [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | |
| [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | | | [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | |
| [Mapper](./puppeteer.mapper.md) | |
| [MouseButton](./puppeteer.mousebutton.md) | | | [MouseButton](./puppeteer.mousebutton.md) | |
| [NodeFor](./puppeteer.nodefor.md) | | | [NodeFor](./puppeteer.nodefor.md) | |
| [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. | | [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. |

View File

@ -0,0 +1,25 @@
---
sidebar_label: Locator.map
---
# Locator.map() method
Maps the locator using the provided mapper.
#### Signature:
```typescript
class Locator {
map<To>(mapper: Mapper<T, To>): Locator<To>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | -------------------------------------------- | ----------- |
| mapper | [Mapper](./puppeteer.mapper.md)&lt;T, To&gt; | |
**Returns:**
[Locator](./puppeteer.locator.md)&lt;To&gt;

View File

@ -27,6 +27,7 @@ export declare abstract class Locator<T> extends EventEmitter
| [click(this, options)](./puppeteer.locator.click.md) | | | | [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. | | [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) | | | | [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) | | | | [off(eventName, handler)](./puppeteer.locator.off.md) | | |
| [on(eventName, handler)](./puppeteer.locator.on.md) | | | | [on(eventName, handler)](./puppeteer.locator.on.md) | | |
| [once(eventName, handler)](./puppeteer.locator.once.md) | | | | [once(eventName, handler)](./puppeteer.locator.once.md) | | |

View File

@ -0,0 +1,13 @@
---
sidebar_label: Mapper
---
# Mapper type
#### Signature:
```typescript
export type Mapper<From, To> = (value: From) => Awaitable<To>;
```
**References:** [Awaitable](./puppeteer.awaitable.md)

View File

@ -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<T, U> extends Locator<U> {
#delegate: Locator<T>;
constructor(delegate: Locator<T>) {
super();
this.#delegate = delegate;
this.copyOptions(this.#delegate);
}
protected get delegate(): Locator<T> {
return this.#delegate;
}
override setTimeout(timeout: number): this {
super.setTimeout(timeout);
this.#delegate.setTimeout(timeout);
return this;
}
override setVisibility<T extends Node, U extends T>(
this: DelegatedLocator<T, U>,
visibility: VisibilityOption
): Locator<U> {
super.setVisibility(visibility);
this.#delegate.setVisibility(visibility);
return this;
}
override setWaitForEnabled<T extends Node, U extends T>(
this: DelegatedLocator<T, U>,
value: boolean
): Locator<U> {
super.setWaitForEnabled(value);
this.#delegate.setWaitForEnabled(value);
return this;
}
override setEnsureElementIsInTheViewport<T extends Element, U extends T>(
this: DelegatedLocator<T, U>,
value: boolean
): Locator<U> {
super.setEnsureElementIsInTheViewport(value);
this.#delegate.setEnsureElementIsInTheViewport(value);
return this;
}
override setWaitForStableBoundingBox<T extends Element, U extends T>(
this: DelegatedLocator<T, U>,
value: boolean
): Locator<U> {
super.setWaitForStableBoundingBox(value);
this.#delegate.setWaitForStableBoundingBox(value);
return this;
}
override _wait(): Observable<HandleFor<U>> {
throw new Error('Not implemented');
}
}

View File

@ -24,7 +24,8 @@ import {
import {Awaitable, HandleFor} from '../../common/common.js'; import {Awaitable, HandleFor} from '../../common/common.js';
import {ElementHandle} from '../ElementHandle.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 * @public
@ -36,69 +37,19 @@ export type Predicate<From, To extends From = From> =
/** /**
* @internal * @internal
*/ */
export class ExpectedLocator<From, To extends From> extends Locator<To> { export class ExpectedLocator<From, To extends From> extends DelegatedLocator<
#base: Locator<From>; From,
To
> {
#predicate: Predicate<From, To>; #predicate: Predicate<From, To>;
constructor(base: Locator<From>, predicate: Predicate<From, To>) { constructor(base: Locator<From>, predicate: Predicate<From, To>) {
super(); super(base);
this.#base = base;
this.#predicate = predicate; this.#predicate = predicate;
this.copyOptions(this.#base);
}
override setTimeout(timeout: number): this {
super.setTimeout(timeout);
this.#base.setTimeout(timeout);
return this;
}
override setVisibility<FromNode extends Node, ToNode extends FromNode>(
this: ExpectedLocator<FromNode, ToNode>,
visibility: VisibilityOption
): Locator<ToNode> {
super.setVisibility(visibility);
this.#base.setVisibility(visibility);
return this;
}
override setWaitForEnabled<FromNode extends Node, ToNode extends FromNode>(
this: ExpectedLocator<FromNode, ToNode>,
value: boolean
): Locator<ToNode> {
super.setWaitForEnabled(value);
this.#base.setWaitForEnabled(value);
return this;
}
override setEnsureElementIsInTheViewport<
FromElement extends Element,
ToElement extends FromElement,
>(
this: ExpectedLocator<FromElement, ToElement>,
value: boolean
): Locator<ToElement> {
super.setEnsureElementIsInTheViewport(value);
this.#base.setEnsureElementIsInTheViewport(value);
return this;
}
override setWaitForStableBoundingBox<
FromElement extends Element,
ToElement extends FromElement,
>(
this: ExpectedLocator<FromElement, ToElement>,
value: boolean
): Locator<ToElement> {
super.setWaitForStableBoundingBox(value);
this.#base.setWaitForStableBoundingBox(value);
return this;
} }
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> { override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.#base._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(

View File

@ -48,6 +48,8 @@ import {
Action, Action,
AwaitedLocator, AwaitedLocator,
ExpectedLocator, ExpectedLocator,
MappedLocator,
Mapper,
Predicate, Predicate,
RaceLocator, RaceLocator,
} from './locators.js'; } from './locators.js';
@ -655,6 +657,15 @@ export abstract class Locator<T> extends EventEmitter {
} }
} }
/**
* Maps the locator using the provided mapper.
*
* @public
*/
map<To>(mapper: Mapper<T, To>): Locator<To> {
return new MappedLocator(this, mapper);
}
/** /**
* Creates an expectation that is evaluated against located values. * Creates an expectation that is evaluated against located values.
* *

View File

@ -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<From, To> = (value: From) => Awaitable<To>;
/**
* @internal
*/
export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
#mapper: Mapper<From, To>;
constructor(base: Locator<From>, mapper: Mapper<From, To>) {
super(base);
this.#mapper = mapper;
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from((handle as JSHandle<From>).evaluateHandle(this.#mapper));
})
);
}
}

View File

@ -18,3 +18,5 @@ export * from './Locator.js';
export * from './NodeLocator.js'; export * from './NodeLocator.js';
export * from './ExpectedLocator.js'; export * from './ExpectedLocator.js';
export * from './RaceLocator.js'; export * from './RaceLocator.js';
export * from './DelegatedLocator.js';
export * from './MappedLocator.js';

View File

@ -540,6 +540,67 @@ describe('Locator', function () {
}); });
}); });
describe('Locator.prototype.map', () => {
it('should work', async () => {
const {page} = await getTestState();
await page.setContent(`<div>test</div>`);
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(`<div>test</div>`);
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(`<div>test</div>`);
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', () => { describe('Locator.prototype.expect', () => {
it('should resolve as soon as the predicate matches', async () => { it('should resolve as soon as the predicate matches', async () => {
const clock = sinon.useFakeTimers({ const clock = sinon.useFakeTimers({