Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2796,3 +2796,50 @@ orderSent.WaitForAsync();

### option: Locator.waitFor.timeout = %%-input-timeout-js-%%
* since: v1.16

## async method: Locator.waitForFunction
* since: v1.62

Returns when [`param: expression`] returns a truthy value, called with the matching element as a first argument, and [`param: arg`] as a second argument.

This is a generic way to wait for an element to reach a custom condition without asserting it. The locator is re-resolved on each retry, so it tolerates the element being re-rendered while waiting.

If [`param: expression`] returns a [Promise], this method will wait for the promise to resolve before checking its value.

If [`param: expression`] throws or rejects, this method throws.

**Usage**

Wait for an attribute to appear:

```js
const toggle = page.getByRole('button', { name: 'Menu' });
await toggle.click();
await toggle.waitForFunction(element => element.hasAttribute('aria-expanded'));
```

Passing argument to [`param: expression`]:

```js
await page.getByTestId('status').waitForFunction((element, value) => {
return element.textContent === value;
}, 'Ready');
```

### param: Locator.waitForFunction.expression = %%-evaluate-expression-%%
* since: v1.62

### param: Locator.waitForFunction.expression = %%-js-evaluate-pagefunction-%%
* since: v1.62

### param: Locator.waitForFunction.arg
* since: v1.62
- `arg` ?<[EvaluationArgument]>

Optional argument to pass to [`param: expression`].

### option: Locator.waitForFunction.timeout = %%-wait-for-function-timeout-%%
* since: v1.62

### option: Locator.waitForFunction.timeout = %%-wait-for-function-timeout-js-%%
* since: v1.62
127 changes: 32 additions & 95 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12852,78 +12852,7 @@ export interface Locator {
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).
* @param options
*/
evaluate<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, R>, arg: Arg, options?: {
timeout?: number;
}): Promise<R>;
/**
* Execute JavaScript code in the page, taking the matching element as an argument.
*
* **Details**
*
* Returns the return value of
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression), called with the
* matching element as a first argument, and
* [`arg`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-arg) as a second argument.
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression) returns a
* [Promise], this method will wait for the promise to resolve and return its value.
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression) throws or
* rejects, this method throws.
*
* **Usage**
*
* Passing argument to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression):
*
* ```js
* const result = await page.getByTestId('myId').evaluate((element, [x, y]) => {
* return element.textContent + ' ' + x * y;
* }, [7, 8]);
* console.log(result); // prints "myId text 56"
* ```
*
* @param pageFunction Function to be evaluated in the page context.
* @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression).
* @param options
*/
evaluate<R, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, void, R>, options?: {
timeout?: number;
}): Promise<R>;
/**
* Execute JavaScript code in the page, taking the matching element as an argument, and return a
* [JSHandle](https://playwright.dev/docs/api/class-jshandle) with the result.
*
* **Details**
*
* Returns the return value of
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression) as
* a[JSHandle](https://playwright.dev/docs/api/class-jshandle), called with the matching element as a first argument,
* and [`arg`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-arg) as a second argument.
*
* The only difference between
* [locator.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate)
* and
* [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle)
* is that
* [locator.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle)
* returns [JSHandle](https://playwright.dev/docs/api/class-jshandle).
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression)
* returns a [Promise], this method will wait for the promise to resolve and return its value.
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression) throws
* or rejects, this method throws.
*
* See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for
* more details.
* @param pageFunction Function to be evaluated in the page context.
* @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression).
* @param options
*/
evaluateHandle<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, R>, arg: Arg): Promise<SmartHandle<R>>;
evaluate<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, R>, arg?: Arg, options?: { timeout?: number }): Promise<R>;
/**
* Execute JavaScript code in the page, taking the matching element as an argument, and return a
* [JSHandle](https://playwright.dev/docs/api/class-jshandle) with the result.
Expand Down Expand Up @@ -12956,7 +12885,7 @@ export interface Locator {
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression).
* @param options
*/
evaluateHandle<R, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, void, R>): Promise<SmartHandle<R>>;
evaluateHandle<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, R>, arg?: Arg, options?: { timeout?: number }): Promise<SmartHandle<R>>;
/**
* Execute JavaScript code in the page, taking all matching elements as an argument.
*
Expand Down Expand Up @@ -12984,35 +12913,47 @@ export interface Locator {
* @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression).
*/
evaluateAll<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E[], Arg, R>, arg: Arg): Promise<R>;
evaluateAll<R, Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E[], Arg, R>, arg?: Arg): Promise<R>;
/**
* Execute JavaScript code in the page, taking all matching elements as an argument.
* Returns when
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-expression) returns
* a truthy value, called with the matching element as a first argument, and
* [`arg`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-arg) as a second argument.
*
* **Details**
* This is a generic way to wait for an element to reach a custom condition without asserting it. The locator is
* re-resolved on each retry, so it tolerates the element being re-rendered while waiting.
*
* Returns the return value of
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression), called with
* an array of all matching elements as a first argument, and
* [`arg`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-arg) as a second argument.
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-expression)
* returns a [Promise], this method will wait for the promise to resolve before checking its value.
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression) returns a
* [Promise], this method will wait for the promise to resolve and return its value.
*
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression) throws or
* rejects, this method throws.
* If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-expression)
* throws or rejects, this method throws.
*
* **Usage**
*
* Wait for an attribute to appear:
*
* ```js
* const locator = page.locator('div');
* const moreThanTen = await locator.evaluateAll((divs, min) => divs.length > min, 10);
* const toggle = page.getByRole('button', { name: 'Menu' });
* await toggle.click();
* await toggle.waitForFunction(element => element.hasAttribute('aria-expanded'));
* ```
*
* Passing argument to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-expression):
*
* ```js
* await page.getByTestId('status').waitForFunction((element, value) => {
* return element.textContent === value;
* }, 'Ready');
* ```
*
* @param pageFunction Function to be evaluated in the page context.
* @param arg Optional argument to pass to
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression).
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-wait-for-function-option-expression).
* @param options
*/
evaluateAll<R, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E[], void, R>): Promise<R>;
waitForFunction<Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, any>, arg?: Arg, options?: { timeout?: number }): Promise<void>;
/**
* **NOTE** Always prefer using [Locator](https://playwright.dev/docs/api/class-locator)s and web assertions over
* [ElementHandle](https://playwright.dev/docs/api/class-elementhandle)s because latter are inherently racy.
Expand All @@ -13021,17 +12962,13 @@ export interface Locator {
* multiple elements match the locator, throws.
* @param options
*/
elementHandle(options?: {
timeout?: number;
}): Promise<null|ElementHandle<SVGElement | HTMLElement>>;
elementHandle(options?: { timeout?: number }): Promise<ElementHandle<SVGElement | HTMLElement>>;
/**
* Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses
* [locator.highlight([options])](https://playwright.dev/docs/api/class-locator#locator-highlight).
* @param options
*/
highlight(options?: {
style?: string | { [key: string]: string | number };
}): Promise<Disposable>;
highlight(options?: { style?: string | { [key: string]: string | number } }): Promise<Disposable>;
/**
* Returns a human-readable representation of the locator, using the
* [locator.description()](https://playwright.dev/docs/api/class-locator#locator-description) if one exists;
Expand Down
6 changes: 5 additions & 1 deletion packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2981,13 +2981,17 @@ export type FrameWaitForFunctionParams = {
arg: SerializedArgument,
timeout: number,
pollingInterval?: number,
selector?: string,
strict?: boolean,
};
export type FrameWaitForFunctionOptions = {
isFunction?: boolean,
pollingInterval?: number,
selector?: string,
strict?: boolean,
};
export type FrameWaitForFunctionResult = {
handle: JSHandleChannel,
handle?: JSHandleChannel,
};
export type FrameWaitForSelectorParams = {
selector: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
arg: serializeArgument(arg),
timeout: this._timeout(options),
});
return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
return JSHandle.from(result.handle!) as any as structs.SmartHandle<R>;
}

async title(): Promise<string> {
Expand Down
13 changes: 13 additions & 0 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { escapeForTextSelector } from '@isomorphic/stringUtils';
import { isString } from '@isomorphic/rtti';
import { monotonicTime } from '@isomorphic/time';
import { ElementHandle } from './elementHandle';
import { serializeArgument } from './jsHandle';
import { DisposableStub } from './disposable';

import type { ExpectResult, Frame } from './frame';
Expand Down Expand Up @@ -390,6 +391,18 @@ export class Locator implements api.Locator {
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options, timeout: this._frame._timeout(options) });
}

async waitForFunction<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<void> {
await this._frame._channel.waitForFunction({
selector: this._selector,
strict: true,
Comment thread
dgozman marked this conversation as resolved.
expression: String(pageFunction),
isFunction: typeof pageFunction === 'function',
arg: serializeArgument(arg),
timeout: this._frame._timeout(options),
pollingInterval: 100,
});
}


async _expect(expression: string, options: FrameExpectParams): Promise<ExpectResult> {
return this._frame._expect(expression, {
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1632,9 +1632,11 @@ scheme.FrameWaitForFunctionParams = tObject({
arg: tType('SerializedArgument'),
timeout: tFloat,
pollingInterval: tOptional(tFloat),
selector: tOptional(tString),
strict: tOptional(tBoolean),
});
scheme.FrameWaitForFunctionResult = tObject({
handle: tChannel(['ElementHandle', 'JSHandle']),
handle: tOptional(tChannel(['ElementHandle', 'JSHandle'])),
});
scheme.FrameWaitForSelectorParams = tObject({
selector: tString,
Expand Down
6 changes: 5 additions & 1 deletion packages/playwright-core/src/server/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2984,13 +2984,17 @@ export type FrameWaitForFunctionParams = {
arg: SerializedArgument,
timeout: number,
pollingInterval?: number,
selector?: string,
strict?: boolean,
};
export type FrameWaitForFunctionOptions = {
isFunction?: boolean,
pollingInterval?: number,
selector?: string,
strict?: boolean,
};
export type FrameWaitForFunctionResult = {
handle: JSHandleChannel,
handle?: JSHandleChannel,
};
export type FrameWaitForSelectorParams = {
selector: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,12 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
}

async waitForFunction(params: channels.FrameWaitForFunctionParams, progress: Progress): Promise<channels.FrameWaitForFunctionResult> {
return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.waitForFunctionExpression(progress, params.expression, params.isFunction, parseArgument(params.arg), params)) };
const handle = await this._frame.waitForFunctionExpression(progress, params.expression, params.isFunction, parseArgument(params.arg), params);
if (params.selector !== undefined) {
handle.dispose();
return {};
}
return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, handle) };
}

async title(params: channels.FrameTitleParams, progress: Progress): Promise<channels.FrameTitleResult> {
Expand Down
39 changes: 30 additions & 9 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,30 +1599,49 @@ export class Frame extends SdkObject<FrameEventMap> {
return { matches, received };
}

async waitForFunctionExpression<R>(progress: Progress, expression: string, isFunction: boolean | undefined, arg: any, options: { pollingInterval?: number }, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
async waitForFunctionExpression<R>(progress: Progress, expression: string, isFunction: boolean | undefined, arg: any, options: { pollingInterval?: number, selector?: string, strict?: boolean }, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
if (typeof options.pollingInterval === 'number')
assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval);
expression = js.normalizeEvaluationExpression(expression, isFunction);
return this.retryWithProgressAndTimeouts(progress, [100], async () => {
const context = world === 'main' ? await progress.race(this.mainContext()) : await progress.race(this.utilityContext());
const injectedScript = await progress.race(context.injectedScript());
const handle = await progress.race(injectedScript.evaluateHandle((injected, { expression, isFunction, polling, arg }) => {
if (options.selector !== undefined)
progress.log(`waiting for ${this._asLocator(options.selector)}`);
return this.retryWithProgressAndBackoff(progress, async (progress, continuePolling) => {
let injectedScript: js.JSHandle<InjectedScript>;
let info: SelectorInfo | undefined;
if (options.selector !== undefined) {
const resolved = await progress.race(this.selectors.resolveInjectedForSelector(options.selector, { strict: options.strict, mainWorld: true }));
if (!resolved)
return continuePolling;
injectedScript = resolved.injected;
info = resolved.info;
} else {
const context = world === 'main' ? await progress.race(this.mainContext()) : await progress.race(this.utilityContext());
injectedScript = await progress.race(context.injectedScript());
}
const handle = await progress.race(injectedScript.evaluateHandle((injected, { info, expression, isFunction, polling, arg }) => {
let evaledExpression: any;
const predicate = (): R => {
const args = [arg];
if (info) {
const element = injected.querySelector(info.parsed, document, info.strict);
if (!element)
return undefined as any;
args.unshift(element);
}
// NOTE: make sure to use `globalThis.eval` instead of `self.eval` due to a bug with sandbox isolation
// in firefox.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1814898
let result = evaledExpression ?? globalThis.eval(expression);
if (isFunction === true) {
evaledExpression = result;
result = result(arg);
result = result(...args);
} else if (isFunction === false) {
result = result;
} else {
// auto detect.
if (typeof result === 'function') {
evaledExpression = result;
result = result(arg);
result = result(...args);
}
}
return result;
Expand Down Expand Up @@ -1653,12 +1672,14 @@ export class Frame extends SdkObject<FrameEventMap> {

next();
return { result, abort: () => aborted = true };
}, { expression, isFunction, polling: options.pollingInterval, arg }));
}, { info, expression, isFunction, polling: options.pollingInterval, arg }));
try {
return await progress.race(handle.evaluateHandle(h => h.result));
} catch (error) {
// Note: it is important to await "abort()" to prevent any side effects
// after this method returns.
// after this method returns. We intentionally do not race against progress
// here - it is already resolved/aborted, and the abort must run to completion.
// eslint-disable-next-line progress/await-must-use-progress
await handle.evaluate(h => h.abort()).catch(() => {});
throw error;
} finally {
Expand Down
Loading
Loading