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
35 changes: 35 additions & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -977,3 +977,38 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Locator.uncheck.noWaitAfter = %%-input-no-wait-after-%%
### option: Locator.uncheck.timeout = %%-input-timeout-%%
### option: Locator.uncheck.trial = %%-input-trial-%%

## async method: Locator.waitFor

Returns when element specified by locator satisfies the [`option: state`] option.

If target element already satisfies the condition, the method returns immediately. Otherwise, waits for up to
[`option: timeout`] milliseconds until the condition is met.

```js
const orderSent = page.locator('#order-sent');
await orderSent.waitFor();
```

```java
Locator orderSent = page.locator("#order-sent");
orderSent.waitFor();
```

```python async
order_sent = page.locator("#order-sent")
await order_sent.wait_for()
```

```python sync
order_sent = page.locator("#order-sent")
order_sent.wait_for()
```

```csharp
var orderSent = page.Locator("#order-sent");
orderSent.WaitForAsync();
```

### option: Locator.waitFor.state = %%-wait-for-selector-state-%%
### option: Locator.waitFor.timeout = %%-input-timeout-%%
8 changes: 8 additions & 0 deletions src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ export class Locator implements api.Locator {
return this._frame.$$eval(this._selector, ee => ee.map(e => e.textContent || ''));
}

waitFor(options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise<void>;
waitFor(options?: channels.FrameWaitForSelectorOptions): Promise<void>;
async waitFor(options?: channels.FrameWaitForSelectorOptions): Promise<void> {
return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => {
await channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
});
}

async _expect(expression: string, options: channels.FrameExpectOptions): Promise<{ pass: boolean, received?: any, log?: string[] }> {
return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => {
const params: any = { selector: this._selector, expression, ...options };
Expand Down
2 changes: 2 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2171,11 +2171,13 @@ export type FrameWaitForSelectorParams = {
strict?: boolean,
timeout?: number,
state?: 'attached' | 'detached' | 'visible' | 'hidden',
omitReturnValue?: boolean,
};
export type FrameWaitForSelectorOptions = {
strict?: boolean,
timeout?: number,
state?: 'attached' | 'detached' | 'visible' | 'hidden',
omitReturnValue?: boolean,
};
export type FrameWaitForSelectorResult = {
element?: ElementHandleChannel,
Expand Down
1 change: 1 addition & 0 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,7 @@ Frame:
- detached
- visible
- hidden
omitReturnValue: boolean?
returns:
element: ElementHandle?
tracing:
Expand Down
1 change: 1 addition & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
strict: tOptional(tBoolean),
timeout: tOptional(tNumber),
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
omitReturnValue: tOptional(tBoolean),
});
scheme.FrameExpectParams = tObject({
selector: tString,
Expand Down
18 changes: 11 additions & 7 deletions src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.parseSelector(selector, options);
const task = waitForSelectorTask(info, state, this);
const task = waitForSelectorTask(info, state, false, this);
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
Expand Down Expand Up @@ -939,13 +939,13 @@ function compensateHalfIntegerRoundingError(point: types.Point) {

export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;

export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden', root?: ElementHandle): SchedulableTask<Element | undefined> {
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, state, root }) => {
export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden', omitReturnValue?: boolean, root?: ElementHandle): SchedulableTask<Element | undefined> {
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, state, omitReturnValue, root }) => {
let lastElement: Element | undefined;

return injected.pollRaf((progress, continuePolling) => {
const elements = injected.querySelectorAll(parsed, root || document);
const element = elements[0];
let element: Element | undefined = elements[0];
const visible = element ? injected.isVisible(element) : false;

if (lastElement !== element) {
Expand All @@ -962,18 +962,22 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
}
}

const hasElement = !!element;
if (omitReturnValue)
element = undefined;

switch (state) {
case 'attached':
return element ? element : continuePolling;
return hasElement ? element : continuePolling;
case 'detached':
return !element ? undefined : continuePolling;
return !hasElement ? undefined : continuePolling;
case 'visible':
return visible ? element : continuePolling;
case 'hidden':
return !visible ? undefined : continuePolling;
}
});
}, { parsed: selector.parsed, strict: selector.strict, state, root });
}, { parsed: selector.parsed, strict: selector.strict, state, omitReturnValue, root });
}

export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document';
4 changes: 2 additions & 2 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ export class Frame extends SdkObject {
return this._page.selectors.query(this, selector, options);
}

async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<dom.ElementHandle<Element> | null> {
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions & { omitReturnValue?: boolean } = {}): Promise<dom.ElementHandle<Element> | null> {
const controller = new ProgressController(metadata, this);
if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?');
Expand All @@ -714,7 +714,7 @@ export class Frame extends SdkObject {
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.parseSelector(selector, options);
const task = dom.waitForSelectorTask(info, state);
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue);
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
while (progress.isRunning()) {
Expand Down
17 changes: 17 additions & 0 deletions tests/page/locator-misc-2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,20 @@ it('should return bounding box', async ({ page, server, browserName, headless, i
const box = await element.boundingBox();
expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 });
});

it('should waitFor', async ({ page }) => {
Comment thread
pavelfeldman marked this conversation as resolved.
await page.setContent(`<div></div>`);
const locator = page.locator('span');
const promise = locator.waitFor();
await page.$eval('div', div => div.innerHTML = '<span>target</span>');
await promise;
await expect(locator).toHaveText('target');
});

it('should waitFor hidden', async ({ page }) => {
await page.setContent(`<div><span>target</span></div>`);
const locator = page.locator('span');
const promise = locator.waitFor({ state: 'hidden' });
await page.$eval('div', div => div.innerHTML = '');
await promise;
});
34 changes: 34 additions & 0 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9551,6 +9551,40 @@ export interface Locator {
* `false`. Useful to wait until the element is ready for the action without performing it.
*/
trial?: boolean;
}): Promise<void>;

/**
* Returns when element specified by locator satisfies the `state` option.
*
* If target element already satisfies the condition, the method returns immediately. Otherwise, waits for up to `timeout`
* milliseconds until the condition is met.
*
* ```js
* const orderSent = page.locator('#order-sent');
* await orderSent.waitFor();
* ```
*
* @param options
*/
waitFor(options?: {
/**
* Defaults to `'visible'`. Can be either:
* - `'attached'` - wait for element to be present in DOM.
* - `'detached'` - wait for element to not be present in DOM.
* - `'visible'` - wait for element to have non-empty bounding box and no `visibility:hidden`. Note that element without
* any content or with `display:none` has an empty bounding box and is not considered visible.
* - `'hidden'` - wait for element to be either detached from DOM, or have an empty bounding box or `visibility:hidden`.
* This is opposite to the `'visible'` option.
*/
state?: "attached"|"detached"|"visible"|"hidden";

/**
* Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by
* using the
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
timeout?: number;
}): Promise<void>;}

/**
Expand Down