api: add waitForElementState('disabled') (#3537)

Allows waiting for the element to be disabled.
This commit is contained in:
Dmitry Gozman 2020-08-19 17:20:10 -07:00 committed by GitHub
parent 0a22e2758a
commit 18292325b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 41 additions and 6 deletions

View file

@ -3113,7 +3113,7 @@ 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.
- `state` <"visible"|"hidden"|"stable"|"enabled"|"disabled"> 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`.
@ -3123,6 +3123,7 @@ Depending on the `state` parameter, this method waits for one of the [actionabil
- `"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).
- `"disabled"` Wait until the element is [not enabled](./actionability.md#enabled).
If the element does not satisfy the condition for the `timeout` milliseconds, this method will throw.

View file

@ -602,8 +602,9 @@ 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> {
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: types.TimeoutOptions = {}): Promise<void> {
return this._page._runAbortableTask(async progress => {
progress.log(` waiting for element to be ${state}`);
if (state === 'visible') {
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
return injected.waitForNodeVisible(node);
@ -628,6 +629,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
return;
}
if (state === 'disabled') {
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
return injected.waitForNodeDisabled(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]) => {

View file

@ -383,6 +383,19 @@ export default class InjectedScript {
});
}
waitForNodeDisabled(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 enabled - waiting...');
return continuePolling;
}
return 'done';
});
}
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' {
if (!node.isConnected)
return 'error:notconnected';

View file

@ -1914,7 +1914,7 @@ export type ElementHandleUncheckOptions = {
};
export type ElementHandleUncheckResult = void;
export type ElementHandleWaitForElementStateParams = {
state: 'visible' | 'hidden' | 'stable' | 'enabled',
state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled',
timeout?: number,
};
export type ElementHandleWaitForElementStateOptions = {

View file

@ -221,7 +221,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
});
}
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled', options: ElementHandleWaitForElementStateOptions = {}): Promise<void> {
async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: ElementHandleWaitForElementStateOptions = {}): Promise<void> {
return this._wrapApiCall('elementHandle.waitForElementState', async () => {
return await this._elementChannel.waitForElementState({ state, ...options });
});

View file

@ -1585,6 +1585,7 @@ ElementHandle:
- hidden
- stable
- enabled
- disabled
timeout: number?
waitForSelector:

View file

@ -150,7 +150,7 @@ 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> {
async waitForElementState(params: { state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' } & types.TimeoutOptions): Promise<void> {
await this._elementHandle.waitForElementState(params.state, params);
}

View file

@ -759,7 +759,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
timeout: tOptional(tNumber),
});
scheme.ElementHandleWaitForElementStateParams = tObject({
state: tEnum(['visible', 'hidden', 'stable', 'enabled']),
state: tEnum(['visible', 'hidden', 'stable', 'enabled', 'disabled']),
timeout: tOptional(tNumber),
});
scheme.ElementHandleWaitForSelectorParams = tObject({

View file

@ -102,6 +102,17 @@ it('should throw waiting for enabled when detached', async ({ page }) => {
expect(error.message).toContain('Element is not attached to the DOM');
});
it('should wait for disabled button', async({page}) => {
await page.setContent('<button><span>Target</span></button>');
const span = await page.$('text=Target');
let done = false;
const promise = span.waitForElementState('disabled').then(() => done = true);
await giveItAChanceToResolve(page);
expect(done).toBe(false);
await span.evaluate(span => (span.parentElement as HTMLButtonElement).disabled = true);
await promise;
});
it('should wait for stable position', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');