feat(api): introduce locator.waitFor (#9200)

This commit is contained in:
Pavel Feldman 2021-09-28 13:57:11 -07:00 committed by GitHub
parent 64657c3b65
commit 2b055b3092
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 111 additions and 9 deletions

View file

@ -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-%%

View file

@ -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 };

View file

@ -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,

View file

@ -1746,6 +1746,7 @@ Frame:
- detached
- visible
- hidden
omitReturnValue: boolean?
returns:
element: ElementHandle?
tracing:

View file

@ -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,

View file

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

View file

@ -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()) {

View file

@ -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
View file

@ -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>;}
/**