diff --git a/docs/api.md b/docs/api.md index e11bbc34c54ec..f746472d05345 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. diff --git a/src/dom.ts b/src/dom.ts index 513cc559d995c..5d6f58e6aa1bc 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -602,8 +602,9 @@ export class ElementHandle extends js.JSHandle { return result; } - async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled', options: types.TimeoutOptions = {}): Promise { + async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: types.TimeoutOptions = {}): Promise { 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 extends js.JSHandle { 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]) => { diff --git a/src/injected/injectedScript.ts b/src/injected/injectedScript.ts index 6754f7b3b1f2e..772778dc37ebe 100644 --- a/src/injected/injectedScript.ts +++ b/src/injected/injectedScript.ts @@ -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'; diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 72773adf871a2..05bd886d3b4ce 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -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 = { diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts index ba3ced28d58c3..625b6063c1caf 100644 --- a/src/rpc/client/elementHandle.ts +++ b/src/rpc/client/elementHandle.ts @@ -221,7 +221,7 @@ export class ElementHandle extends JSHandle { }); } - async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled', options: ElementHandleWaitForElementStateOptions = {}): Promise { + async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: ElementHandleWaitForElementStateOptions = {}): Promise { return this._wrapApiCall('elementHandle.waitForElementState', async () => { return await this._elementChannel.waitForElementState({ state, ...options }); }); diff --git a/src/rpc/protocol.yml b/src/rpc/protocol.yml index 59538a62ce600..f06c450767aec 100644 --- a/src/rpc/protocol.yml +++ b/src/rpc/protocol.yml @@ -1585,6 +1585,7 @@ ElementHandle: - hidden - stable - enabled + - disabled timeout: number? waitForSelector: diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts index 9d46ce930a1e8..8747025ff5672 100644 --- a/src/rpc/server/elementHandlerDispatcher.ts +++ b/src/rpc/server/elementHandlerDispatcher.ts @@ -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 { + async waitForElementState(params: { state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' } & types.TimeoutOptions): Promise { await this._elementHandle.waitForElementState(params.state, params); } diff --git a/src/rpc/validator.ts b/src/rpc/validator.ts index c5a780d4fd71e..7efba25d75778 100644 --- a/src/rpc/validator.ts +++ b/src/rpc/validator.ts @@ -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({ diff --git a/test/elementhandle-wait-for-element-state.spec.ts b/test/elementhandle-wait-for-element-state.spec.ts index 49adeeea7a03f..2bcdfd7bce3e4 100644 --- a/test/elementhandle-wait-for-element-state.spec.ts +++ b/test/elementhandle-wait-for-element-state.spec.ts @@ -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(''); + 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');