mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
api: ElementHandle.waitForElementState (#3501)
This method waits for visible, hidden, stable or enabled state, similar to the actionability checks performed before actions. This gives a bit more control to the user. Some examples: - Allows to wait for something to be stable before taking a screenshot. - Allows to wait for the element to be hidden/detached after a specific action.
This commit is contained in:
parent
58fc6b4003
commit
0e9793c452
18
docs/api.md
18
docs/api.md
@ -2748,6 +2748,7 @@ ElementHandle instances can be used as an argument in [`page.$eval()`](#pageeval
|
||||
- [elementHandle.toString()](#elementhandletostring)
|
||||
- [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
||||
- [elementHandle.uncheck([options])](#elementhandleuncheckoptions)
|
||||
- [elementHandle.waitForElementState(state[, options])](#elementhandlewaitforelementstatestate-options)
|
||||
- [elementHandle.waitForSelector(selector[, options])](#elementhandlewaitforselectorselector-options)
|
||||
<!-- GEN:stop -->
|
||||
<!-- GEN:toc-extends-JSHandle -->
|
||||
@ -3111,6 +3112,21 @@ If the element is detached from the DOM at any moment during the action, this me
|
||||
|
||||
When all steps combined have not finished during the specified `timeout`, this method rejects with a [TimeoutError]. Passing zero timeout disables this.
|
||||
|
||||
#### elementHandle.waitForElementState(state[, options])
|
||||
- `state` <"visible"|"hidden"|"stable"|"enabled"> A state to wait for, see below for more details.
|
||||
- `options` <[Object]>
|
||||
- `timeout` <[number]> 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)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
|
||||
- returns: <[Promise]> Promise that resolves when the element satisfies the `state`.
|
||||
|
||||
Depending on the `state` parameter, this method waits for one of the [actionability](./actionability.md) checks to pass. This method throws when the element is detached while waiting, unless waiting for the `"hidden"` state.
|
||||
- `"visible"` Wait until the element is [visible](./actionability.md#visible).
|
||||
- `"hidden"` Wait until the element is [not visible](./actionability.md#visible) or [not attached](./actionability.md#attached). Note that waiting for hidden does not throw when the element detaches.
|
||||
- `"stable"` Wait until the element is both [visible](./actionability.md#visible) and [stable](./actionability.md#stable).
|
||||
- `"enabled"` Wait until the element is [enabled](./actionability.md#enabled).
|
||||
|
||||
If the element does not satisfy the condition for the `timeout` milliseconds, this method will throw.
|
||||
|
||||
|
||||
#### elementHandle.waitForSelector(selector[, options])
|
||||
- `selector` <[string]> A selector of an element to wait for, relative to the element handle. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
@ -3131,7 +3147,7 @@ const div = await page.$('div');
|
||||
const span = await div.waitForSelector('span', { state: 'attached' });
|
||||
```
|
||||
|
||||
> **NOTE** This method works does not work across navigations, use [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) instead.
|
||||
> **NOTE** This method does not work across navigations, use [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) instead.
|
||||
|
||||
### class: JSHandle
|
||||
|
||||
|
47
src/dom.ts
47
src/dom.ts
@ -226,14 +226,6 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
private async _waitForVisible(progress: Progress): Promise<'error:notconnected' | 'done'> {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeVisible(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
return throwFatalDOMError(await pollHandler.finish());
|
||||
}
|
||||
|
||||
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport'> {
|
||||
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
||||
return quad.map(point => ({
|
||||
@ -623,6 +615,45 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled', options: types.TimeoutOptions = {}): Promise<void> {
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
if (state === 'visible') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeVisible(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'hidden') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeHidden(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(await pollHandler.finish());
|
||||
return;
|
||||
}
|
||||
if (state === 'enabled') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeEnabled(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'stable') {
|
||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
||||
return injected.waitForDisplayedAtStablePosition(node, rafCount, false /* waitForEnabled */);
|
||||
}, rafCount);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
throw new Error(`state: expected one of (visible|hidden|stable|enabled)`);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
|
||||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
|
@ -357,6 +357,32 @@ export default class InjectedScript {
|
||||
});
|
||||
}
|
||||
|
||||
waitForNodeHidden(node: Node): types.InjectedScriptPoll<'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'done';
|
||||
if (this.isVisible(element)) {
|
||||
progress.logRepeating(' element is visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
waitForNodeEnabled(node: Node): types.InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'error:notconnected';
|
||||
if (this._isElementDisabled(element)) {
|
||||
progress.logRepeating(' element is not enabled - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' {
|
||||
if (!node.isConnected)
|
||||
return 'error:notconnected';
|
||||
@ -463,8 +489,7 @@ export default class InjectedScript {
|
||||
const style = element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element) : undefined;
|
||||
const isVisible = !!style && style.visibility !== 'hidden';
|
||||
|
||||
const elementOrButton = element.closest('button, [role=button]') || element;
|
||||
const isDisabled = waitForEnabled && ['BUTTON', 'INPUT', 'SELECT'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
||||
const isDisabled = waitForEnabled && this._isElementDisabled(element);
|
||||
|
||||
if (isDisplayed && isStable && isVisible && !isDisabled)
|
||||
return 'done';
|
||||
@ -526,6 +551,11 @@ export default class InjectedScript {
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
private _isElementDisabled(element: Element): boolean {
|
||||
const elementOrButton = element.closest('button, [role=button]') || element;
|
||||
return ['BUTTON', 'INPUT', 'SELECT'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
||||
}
|
||||
|
||||
private _parentElementOrShadowHost(element: Element): Element | undefined {
|
||||
if (element.parentElement)
|
||||
return element.parentElement;
|
||||
|
@ -1620,6 +1620,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
|
||||
textContent(params?: ElementHandleTextContentParams): Promise<ElementHandleTextContentResult>;
|
||||
type(params: ElementHandleTypeParams): Promise<ElementHandleTypeResult>;
|
||||
uncheck(params: ElementHandleUncheckParams): Promise<ElementHandleUncheckResult>;
|
||||
waitForElementState(params: ElementHandleWaitForElementStateParams): Promise<ElementHandleWaitForElementStateResult>;
|
||||
waitForSelector(params: ElementHandleWaitForSelectorParams): Promise<ElementHandleWaitForSelectorResult>;
|
||||
}
|
||||
export type ElementHandleEvalOnSelectorParams = {
|
||||
@ -1914,6 +1915,14 @@ export type ElementHandleUncheckOptions = {
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleUncheckResult = void;
|
||||
export type ElementHandleWaitForElementStateParams = {
|
||||
state: 'visible' | 'hidden' | 'stable' | 'enabled',
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleWaitForElementStateOptions = {
|
||||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleWaitForElementStateResult = void;
|
||||
export type ElementHandleWaitForSelectorParams = {
|
||||
selector: string,
|
||||
timeout?: number,
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ElementHandleChannel, JSHandleInitializer, ElementHandleScrollIntoViewIfNeededOptions, ElementHandleHoverOptions, ElementHandleClickOptions, ElementHandleDblclickOptions, ElementHandleFillOptions, ElementHandleSetInputFilesOptions, ElementHandlePressOptions, ElementHandleCheckOptions, ElementHandleUncheckOptions, ElementHandleScreenshotOptions, ElementHandleTypeOptions, ElementHandleSelectTextOptions, ElementHandleWaitForSelectorOptions } from '../channels';
|
||||
import { ElementHandleChannel, JSHandleInitializer, ElementHandleScrollIntoViewIfNeededOptions, ElementHandleHoverOptions, ElementHandleClickOptions, ElementHandleDblclickOptions, ElementHandleFillOptions, ElementHandleSetInputFilesOptions, ElementHandlePressOptions, ElementHandleCheckOptions, ElementHandleUncheckOptions, ElementHandleScreenshotOptions, ElementHandleTypeOptions, ElementHandleSelectTextOptions, ElementHandleWaitForSelectorOptions, ElementHandleWaitForElementStateOptions } from '../channels';
|
||||
import { Frame } from './frame';
|
||||
import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
@ -209,6 +209,12 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
|
||||
});
|
||||
}
|
||||
|
||||
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled', options: ElementHandleWaitForElementStateOptions = {}): Promise<void> {
|
||||
return this._wrapApiCall('elementHandle.waitForElementState', async () => {
|
||||
return await this._elementChannel.waitForElementState({ state, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: ElementHandleWaitForSelectorOptions = {}): Promise<ElementHandle<Element> | null> {
|
||||
return this._wrapApiCall('elementHandle.waitForSelector', async () => {
|
||||
const result = await this._elementChannel.waitForSelector({ selector, ...options });
|
||||
|
@ -1577,6 +1577,17 @@ ElementHandle:
|
||||
noWaitAfter: boolean?
|
||||
timeout: number?
|
||||
|
||||
waitForElementState:
|
||||
parameters:
|
||||
state:
|
||||
type: enum
|
||||
literals:
|
||||
- visible
|
||||
- hidden
|
||||
- stable
|
||||
- enabled
|
||||
timeout: number?
|
||||
|
||||
waitForSelector:
|
||||
parameters:
|
||||
selector: string
|
||||
|
@ -149,6 +149,10 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
|
||||
return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
}
|
||||
|
||||
async waitForElementState(params: { state: 'visible' | 'hidden' | 'stable' | 'enabled' } & types.TimeoutOptions): Promise<void> {
|
||||
await this._elementHandle.waitForElementState(params.state, params);
|
||||
}
|
||||
|
||||
async waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element?: ElementHandleChannel }> {
|
||||
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) };
|
||||
}
|
||||
|
@ -759,6 +759,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleWaitForElementStateParams = tObject({
|
||||
state: tEnum(['visible', 'hidden', 'stable', 'enabled']),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleWaitForSelectorParams = tObject({
|
||||
selector: tString,
|
||||
timeout: tOptional(tNumber),
|
||||
|
118
test/elementhandle-wait-for-element-state.spec.ts
Normal file
118
test/elementhandle-wait-for-element-state.spec.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import './base.fixture';
|
||||
|
||||
async function giveItAChanceToResolve(page) {
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
|
||||
}
|
||||
|
||||
it('should wait for visible', async ({ page }) => {
|
||||
await page.setContent(`<div style='display:none'>content</div>`);
|
||||
const div = await page.$('div');
|
||||
let done = false;
|
||||
const promise = div.waitForElementState('visible').then(() => done = true);
|
||||
await giveItAChanceToResolve(page);
|
||||
expect(done).toBe(false);
|
||||
await div.evaluate(div => div.style.display = 'block');
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should wait for already visible', async ({ page }) => {
|
||||
await page.setContent(`<div>content</div>`);
|
||||
const div = await page.$('div');
|
||||
await div.waitForElementState('visible');
|
||||
});
|
||||
|
||||
it('should timeout waiting for visible', async ({ page }) => {
|
||||
await page.setContent(`<div style='display:none'>content</div>`);
|
||||
const div = await page.$('div');
|
||||
const error = await div.waitForElementState('visible', { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('Timeout 1000ms exceeded');
|
||||
});
|
||||
|
||||
it('should throw waiting for visible when detached', async ({ page }) => {
|
||||
await page.setContent(`<div style='display:none'>content</div>`);
|
||||
const div = await page.$('div');
|
||||
const promise = div.waitForElementState('visible').catch(e => e);
|
||||
await div.evaluate(div => div.remove());
|
||||
const error = await promise;
|
||||
expect(error.message).toContain('Element is not attached to the DOM');
|
||||
});
|
||||
|
||||
it('should wait for hidden', async ({ page }) => {
|
||||
await page.setContent(`<div>content</div>`);
|
||||
const div = await page.$('div');
|
||||
let done = false;
|
||||
const promise = div.waitForElementState('hidden').then(() => done = true);
|
||||
await giveItAChanceToResolve(page);
|
||||
expect(done).toBe(false);
|
||||
await div.evaluate(div => div.style.display = 'none');
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should wait for already hidden', async ({ page }) => {
|
||||
await page.setContent(`<div></div>`);
|
||||
const div = await page.$('div');
|
||||
await div.waitForElementState('hidden');
|
||||
});
|
||||
|
||||
it('should wait for hidden when detached', async ({ page }) => {
|
||||
await page.setContent(`<div>content</div>`);
|
||||
const div = await page.$('div');
|
||||
let done = false;
|
||||
const promise = div.waitForElementState('hidden').then(() => done = true);
|
||||
await giveItAChanceToResolve(page);
|
||||
expect(done).toBe(false);
|
||||
await div.evaluate(div => div.remove());
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should wait for enabled button', async({page, server}) => {
|
||||
await page.setContent('<button disabled><span>Target</span></button>');
|
||||
const span = await page.$('text=Target');
|
||||
let done = false;
|
||||
const promise = span.waitForElementState('enabled').then(() => done = true);
|
||||
await giveItAChanceToResolve(page);
|
||||
expect(done).toBe(false);
|
||||
await span.evaluate(span => (span.parentElement as HTMLButtonElement).disabled = false);
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should throw waiting for enabled when detached', async ({ page }) => {
|
||||
await page.setContent(`<button disabled>Target</button>`);
|
||||
const button = await page.$('button');
|
||||
const promise = button.waitForElementState('enabled').catch(e => e);
|
||||
await button.evaluate(button => button.remove());
|
||||
const error = await promise;
|
||||
expect(error.message).toContain('Element is not attached to the DOM');
|
||||
});
|
||||
|
||||
it('should wait for stable position', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/button.html');
|
||||
const button = await page.$('button');
|
||||
await page.$eval('button', button => {
|
||||
button.style.transition = 'margin 10000ms linear 0s';
|
||||
button.style.marginLeft = '20000px';
|
||||
});
|
||||
let done = false;
|
||||
const promise = button.waitForElementState('stable').then(() => done = true);
|
||||
await giveItAChanceToResolve(page);
|
||||
expect(done).toBe(false);
|
||||
await button.evaluate(button => button.style.transition = '');
|
||||
await promise;
|
||||
});
|
Loading…
Reference in New Issue
Block a user