Skip to content
Open
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 @@ -2794,3 +2794,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
84 changes: 84 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12983,6 +12983,90 @@ export interface Locator {
* [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-all-option-expression).
*/
evaluateAll<R, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E[], void, R>): Promise<R>;
/**
* 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.
*
* 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 [`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-wait-for-function-option-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
* [`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-wait-for-function-option-expression).
* @param options
*/
waitForFunction<Arg, E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, Arg, any>, arg: Arg, options?: {
timeout?: number;
}): Promise<void>;
/**
* 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.
*
* 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 [`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-wait-for-function-option-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
* [`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-wait-for-function-option-expression).
* @param options
*/
waitForFunction<E extends SVGElement | HTMLElement = SVGElement | HTMLElement>(pageFunction: PageFunctionOn<E, void, any>, 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 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 @@ -2972,13 +2972,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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be configurable?

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 @@ -1627,9 +1627,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 @@ -2975,13 +2975,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 @@ -1598,30 +1598,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 @@ -1652,12 +1671,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