Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: add waitForElementState('disabled') #3537

Merged
merged 1 commit into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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