Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: implement mapHandle and filterHandle #10716

Merged
merged 1 commit into from Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 14 additions & 8 deletions packages/puppeteer-core/src/api/locators/FilteredLocator.ts
Expand Up @@ -16,13 +16,13 @@

import {
Observable,
filter,
from,
map,
mergeMap,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/common.js';
import {ElementHandle} from '../ElementHandle.js';

import {DelegatedLocator} from './DelegatedLocator.js';
import {ActionOptions, Locator} from './locators.js';
Expand All @@ -34,16 +34,23 @@ export type Predicate<From, To extends From = From> =
| ((value: From) => value is To)
| ((value: From) => Awaitable<boolean>);

/**
* @internal
*/
export type HandlePredicate<From, To extends From = From> =
| ((value: HandleFor<From>, signal?: AbortSignal) => value is HandleFor<To>)
| ((value: HandleFor<From>, signal?: AbortSignal) => Awaitable<boolean>);

/**
* @internal
*/
export class FilteredLocator<From, To extends From> extends DelegatedLocator<
From,
To
> {
#predicate: Predicate<From, To>;
#predicate: HandlePredicate<From, To>;

constructor(base: Locator<From>, predicate: Predicate<From, To>) {
constructor(base: Locator<From>, predicate: HandlePredicate<From, To>) {
super(base);
this.#predicate = predicate;
}
Expand All @@ -59,12 +66,11 @@ export class FilteredLocator<From, To extends From> extends DelegatedLocator<
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from(
(handle as ElementHandle<Node>).frame.waitForFunction(
this.#predicate,
{signal: options?.signal, timeout: this._timeout},
handle
)
Promise.resolve(this.#predicate(handle, options?.signal))
).pipe(
filter(value => {
return value;
}),
map(() => {
// SAFETY: It passed the predicate, so this is correct.
return handle as HandleFor<To>;
Expand Down
35 changes: 34 additions & 1 deletion packages/puppeteer-core/src/api/locators/Locator.ts
Expand Up @@ -48,6 +48,7 @@ import {
Action,
AwaitedLocator,
FilteredLocator,
HandleMapper,
MappedLocator,
Mapper,
Predicate,
Expand Down Expand Up @@ -702,7 +703,10 @@ export abstract class Locator<T> extends EventEmitter {
* @public
*/
map<To>(mapper: Mapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), mapper);
return new MappedLocator(this._clone(), handle => {
// SAFETY: TypeScript cannot deduce the type.
return (handle as any).evaluateHandle(mapper);
});
}

/**
Expand All @@ -713,9 +717,38 @@ export abstract class Locator<T> extends EventEmitter {
* @public
*/
filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
return new FilteredLocator(this._clone(), async (handle, signal) => {
await (handle as ElementHandle<Node>).frame.waitForFunction(
predicate,
{signal, timeout: this._timeout},
handle
);
return true;
});
}

/**
* Creates an expectation that is evaluated against located handles.
*
* If the expectations do not match, then the locator will retry.
*
* @internal
*/
filterHandle<S extends T>(
predicate: Predicate<HandleFor<T>, HandleFor<S>>
): Locator<S> {
return new FilteredLocator(this._clone(), predicate);
}

/**
* Maps the locator using the provided mapper.
*
* @internal
*/
mapHandle<To>(mapper: HandleMapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), mapper);
}

click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
Expand Down
15 changes: 11 additions & 4 deletions packages/puppeteer-core/src/api/locators/MappedLocator.ts
Expand Up @@ -16,7 +16,6 @@

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';

Expand All @@ -25,13 +24,21 @@ import {ActionOptions, DelegatedLocator, Locator} from './locators.js';
*/
export type Mapper<From, To> = (value: From) => Awaitable<To>;

/**
* @internal
*/
export type HandleMapper<From, To> = (
value: HandleFor<From>,
signal?: AbortSignal
) => Awaitable<HandleFor<To>>;

/**
* @internal
*/
export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
#mapper: Mapper<From, To>;
#mapper: HandleMapper<From, To>;

constructor(base: Locator<From>, mapper: Mapper<From, To>) {
constructor(base: Locator<From>, mapper: HandleMapper<From, To>) {
super(base);
this.#mapper = mapper;
}
Expand All @@ -45,7 +52,7 @@ export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
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));
return from(Promise.resolve(this.#mapper(handle, options?.signal)));
})
);
}
Expand Down