Skip to content

Commit

Permalink
api: add waitForElementState('disabled') (#3537)
Browse files Browse the repository at this point in the history
Allows waiting for the element to be disabled.
  • Loading branch information
dgozman authored Aug 20, 2020
1 parent 0a22e27 commit 1829232
Show file tree
Hide file tree
Showing 9 changed files with 41 additions and 6 deletions.
3 changes: 2 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.

Expand Down
11 changes: 10 additions & 1 deletion src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]) => {
Expand Down
13 changes: 13 additions & 0 deletions src/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down
1 change: 1 addition & 0 deletions src/rpc/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,7 @@ ElementHandle:
- hidden
- stable
- enabled
- disabled
timeout: number?

waitForSelector:
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/server/elementHandlerDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion src/rpc/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
11 changes: 11 additions & 0 deletions test/elementhandle-wait-for-element-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 1829232

Please sign in to comment.