feat(api): introduce locator.waitFor (#9200)
This commit is contained in:
parent
64657c3b65
commit
2b055b3092
|
|
@ -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-%%
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1746,6 +1746,7 @@ Frame:
|
|||
- detached
|
||||
- visible
|
||||
- hidden
|
||||
omitReturnValue: boolean?
|
||||
returns:
|
||||
element: ElementHandle?
|
||||
tracing:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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?');
|
||||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
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
types/types.d.ts
vendored
34
types/types.d.ts
vendored
|
|
@ -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>;}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue