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.noWaitAfter = %%-input-no-wait-after-%%
### option: Locator.uncheck.timeout = %%-input-timeout-%% ### option: Locator.uncheck.timeout = %%-input-timeout-%%
### option: Locator.uncheck.trial = %%-input-trial-%% ### 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 || '')); 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[] }> { async _expect(expression: string, options: channels.FrameExpectOptions): Promise<{ pass: boolean, received?: any, log?: string[] }> {
return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => { return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => {
const params: any = { selector: this._selector, expression, ...options }; const params: any = { selector: this._selector, expression, ...options };

View file

@ -2171,11 +2171,13 @@ export type FrameWaitForSelectorParams = {
strict?: boolean, strict?: boolean,
timeout?: number, timeout?: number,
state?: 'attached' | 'detached' | 'visible' | 'hidden', state?: 'attached' | 'detached' | 'visible' | 'hidden',
omitReturnValue?: boolean,
}; };
export type FrameWaitForSelectorOptions = { export type FrameWaitForSelectorOptions = {
strict?: boolean, strict?: boolean,
timeout?: number, timeout?: number,
state?: 'attached' | 'detached' | 'visible' | 'hidden', state?: 'attached' | 'detached' | 'visible' | 'hidden',
omitReturnValue?: boolean,
}; };
export type FrameWaitForSelectorResult = { export type FrameWaitForSelectorResult = {
element?: ElementHandleChannel, element?: ElementHandleChannel,

View file

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

View file

@ -881,6 +881,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
strict: tOptional(tBoolean), strict: tOptional(tBoolean),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])), state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
omitReturnValue: tOptional(tBoolean),
}); });
scheme.FrameExpectParams = tObject({ scheme.FrameExpectParams = tObject({
selector: tString, 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)) if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`); throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.parseSelector(selector, options); 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); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); 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 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> { 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, root }) => { return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, state, omitReturnValue, root }) => {
let lastElement: Element | undefined; let lastElement: Element | undefined;
return injected.pollRaf((progress, continuePolling) => { return injected.pollRaf((progress, continuePolling) => {
const elements = injected.querySelectorAll(parsed, root || document); const elements = injected.querySelectorAll(parsed, root || document);
const element = elements[0]; let element: Element | undefined = elements[0];
const visible = element ? injected.isVisible(element) : false; const visible = element ? injected.isVisible(element) : false;
if (lastElement !== element) { if (lastElement !== element) {
@ -962,18 +962,22 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
} }
} }
const hasElement = !!element;
if (omitReturnValue)
element = undefined;
switch (state) { switch (state) {
case 'attached': case 'attached':
return element ? element : continuePolling; return hasElement ? element : continuePolling;
case 'detached': case 'detached':
return !element ? undefined : continuePolling; return !hasElement ? undefined : continuePolling;
case 'visible': case 'visible':
return visible ? element : continuePolling; return visible ? element : continuePolling;
case 'hidden': case 'hidden':
return !visible ? undefined : continuePolling; 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'; 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); 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); const controller = new ProgressController(metadata, this);
if ((options as any).visibility) if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?'); 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)) if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`); throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const info = this._page.parseSelector(selector, options); 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 => { return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
while (progress.isRunning()) { 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(); const box = await element.boundingBox();
expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 }); 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. * `false`. Useful to wait until the element is ready for the action without performing it.
*/ */
trial?: boolean; 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>;} }): Promise<void>;}
/** /**