diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index ba643d7fdf270..e1c926b40ad3b 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -327,6 +327,11 @@ Returns the `element.innerHTML`. Returns the `element.innerText`. +## async method: ElementHandle.isChecked +- returns: <[boolean]> + +Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + ## async method: ElementHandle.isDisabled - returns: <[boolean]> diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index f16872ae80f80..c4bb7ba16f09b 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -536,6 +536,15 @@ Returns `element.innerText`. ### option: Frame.innerText.timeout = %%-input-timeout-%% +## async method: Frame.isChecked +- returns: <[boolean]> + +Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + +### param: Frame.isChecked.selector = %%-input-selector-%% + +### option: Frame.isChecked.timeout = %%-input-timeout-%% + ## method: Frame.isDetached - returns: <[boolean]> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 6397d42135a1f..24edf00ecd20c 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1002,6 +1002,15 @@ Returns `element.innerText`. ### option: Page.innerText.timeout = %%-input-timeout-%% +## async method: Page.isChecked +- returns: <[boolean]> + +Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + +### param: Page.isChecked.selector = %%-input-selector-%% + +### option: Page.isChecked.timeout = %%-input-timeout-%% + ## method: Page.isClosed - returns: <[boolean]> diff --git a/src/client/elementHandle.ts b/src/client/elementHandle.ts index 52275714d2fd2..31f2cccdf4f86 100644 --- a/src/client/elementHandle.ts +++ b/src/client/elementHandle.ts @@ -87,6 +87,12 @@ export class ElementHandle extends JSHandle implements }); } + async isChecked(): Promise { + return this._wrapApiCall('elementHandle.isChecked', async () => { + return (await this._elementChannel.isChecked()).value; + }); + } + async isDisabled(): Promise { return this._wrapApiCall('elementHandle.isDisabled', async () => { return (await this._elementChannel.isDisabled()).value; diff --git a/src/client/frame.ts b/src/client/frame.ts index 3c78f6772427c..c4a5f2b85f496 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -362,6 +362,12 @@ export class Frame extends ChannelOwner { + return this._wrapApiCall(this._apiName('isChecked'), async () => { + return (await this._channel.isChecked({ selector, ...options })).value; + }); + } + async isDisabled(selector: string, options: channels.FrameIsDisabledOptions = {}): Promise { return this._wrapApiCall(this._apiName('isDisabled'), async () => { return (await this._channel.isDisabled({ selector, ...options })).value; diff --git a/src/client/page.ts b/src/client/page.ts index 86244fbc1a1ff..c6be7904c7145 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -530,6 +530,10 @@ export class Page extends ChannelOwner this._mainFrame.getAttribute(selector, name, options)); } + async isChecked(selector: string, options?: channels.FrameIsCheckedOptions): Promise { + return this._attributeToPage(() => this._mainFrame.isChecked(selector, options)); + } + async isDisabled(selector: string, options?: channels.FrameIsDisabledOptions): Promise { return this._attributeToPage(() => this._mainFrame.isDisabled(selector, options)); } diff --git a/src/dispatchers/elementHandlerDispatcher.ts b/src/dispatchers/elementHandlerDispatcher.ts index a9e21e5e3feb6..59a03504b178f 100644 --- a/src/dispatchers/elementHandlerDispatcher.ts +++ b/src/dispatchers/elementHandlerDispatcher.ts @@ -66,6 +66,10 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann return { value: await this._elementHandle.innerHTML() }; } + async isChecked(): Promise { + return { value: await this._elementHandle.isChecked() }; + } + async isDisabled(): Promise { return { value: await this._elementHandle.isDisabled() }; } diff --git a/src/dispatchers/frameDispatcher.ts b/src/dispatchers/frameDispatcher.ts index 5bdea94c75af6..e249fe5290ba0 100644 --- a/src/dispatchers/frameDispatcher.ts +++ b/src/dispatchers/frameDispatcher.ts @@ -159,6 +159,10 @@ export class FrameDispatcher extends Dispatcher { + return { value: await this._frame.isChecked(params.selector, params) }; + } + async isDisabled(params: channels.FrameIsDisabledParams): Promise { return { value: await this._frame.isDisabled(params.selector, params) }; } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 50846a9a42624..aea4e3a96077b 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -1190,6 +1190,7 @@ export interface FrameChannel extends Channel { hover(params: FrameHoverParams, metadata?: Metadata): Promise; innerHTML(params: FrameInnerHTMLParams, metadata?: Metadata): Promise; innerText(params: FrameInnerTextParams, metadata?: Metadata): Promise; + isChecked(params: FrameIsCheckedParams, metadata?: Metadata): Promise; isDisabled(params: FrameIsDisabledParams, metadata?: Metadata): Promise; isEnabled(params: FrameIsEnabledParams, metadata?: Metadata): Promise; isHidden(params: FrameIsHiddenParams, metadata?: Metadata): Promise; @@ -1446,6 +1447,16 @@ export type FrameInnerTextOptions = { export type FrameInnerTextResult = { value: string, }; +export type FrameIsCheckedParams = { + selector: string, + timeout?: number, +}; +export type FrameIsCheckedOptions = { + timeout?: number, +}; +export type FrameIsCheckedResult = { + value: boolean, +}; export type FrameIsDisabledParams = { selector: string, timeout?: number, @@ -1783,6 +1794,7 @@ export interface ElementHandleChannel extends JSHandleChannel { hover(params: ElementHandleHoverParams, metadata?: Metadata): Promise; innerHTML(params?: ElementHandleInnerHTMLParams, metadata?: Metadata): Promise; innerText(params?: ElementHandleInnerTextParams, metadata?: Metadata): Promise; + isChecked(params?: ElementHandleIsCheckedParams, metadata?: Metadata): Promise; isDisabled(params?: ElementHandleIsDisabledParams, metadata?: Metadata): Promise; isEditable(params?: ElementHandleIsEditableParams, metadata?: Metadata): Promise; isEnabled(params?: ElementHandleIsEnabledParams, metadata?: Metadata): Promise; @@ -1942,6 +1954,11 @@ export type ElementHandleInnerTextOptions = {}; export type ElementHandleInnerTextResult = { value: string, }; +export type ElementHandleIsCheckedParams = {}; +export type ElementHandleIsCheckedOptions = {}; +export type ElementHandleIsCheckedResult = { + value: boolean, +}; export type ElementHandleIsDisabledParams = {}; export type ElementHandleIsDisabledOptions = {}; export type ElementHandleIsDisabledResult = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 015f29182c41d..7559d3e2a9b1f 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -1218,6 +1218,13 @@ Frame: returns: value: string + isChecked: + parameters: + selector: string + timeout: number? + returns: + value: boolean + isDisabled: parameters: selector: string @@ -1637,6 +1644,10 @@ ElementHandle: returns: value: string + isChecked: + returns: + value: boolean + isDisabled: returns: value: boolean diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index af24d7509f9b4..12f11bd1a509a 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -578,6 +578,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { selector: tString, timeout: tOptional(tNumber), }); + scheme.FrameIsCheckedParams = tObject({ + selector: tString, + timeout: tOptional(tNumber), + }); scheme.FrameIsDisabledParams = tObject({ selector: tString, timeout: tOptional(tNumber), @@ -770,6 +774,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.ElementHandleInnerHTMLParams = tOptional(tObject({})); scheme.ElementHandleInnerTextParams = tOptional(tObject({})); + scheme.ElementHandleIsCheckedParams = tOptional(tObject({})); scheme.ElementHandleIsDisabledParams = tOptional(tObject({})); scheme.ElementHandleIsEditableParams = tOptional(tObject({})); scheme.ElementHandleIsEnabledParams = tOptional(tObject({})); diff --git a/src/server/dom.ts b/src/server/dom.ts index ada660e059f81..b2f525cad72b3 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -664,6 +664,12 @@ export class ElementHandle extends js.JSHandle { }, {}); } + async isChecked(): Promise { + return this._evaluateInUtility(([injected, node]) => { + return injected.isCheckboxChecked(node); + }, {}); + } + async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise { return runAbortableTask(async progress => { progress.log(` waiting for element to be ${state}`); @@ -1034,3 +1040,15 @@ export function editableTask(selector: SelectorInfo): SchedulableTask { }); }, selector.parsed); } + +export function checkedTask(selector: SelectorInfo): SchedulableTask { + return injectedScript => injectedScript.evaluateHandle((injected, parsed) => { + return injected.pollRaf((progress, continuePolling) => { + const element = injected.querySelector(parsed, document); + if (!element) + return continuePolling; + progress.log(` selector resolved to ${injected.previewNode(element)}`); + return injected.isCheckboxChecked(element); + }); + }, selector.parsed); +} diff --git a/src/server/frames.ts b/src/server/frames.ts index bd2cb8a022d88..fa9f73a3cc344 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -963,6 +963,15 @@ export class Frame extends EventEmitter { }, this._page._timeoutSettings.timeout(options)); } + async isChecked(selector: string, options: types.TimeoutOptions = {}): Promise { + const info = this._page.selectors._parseSelector(selector); + const task = dom.checkedTask(info); + return runAbortableTask(async progress => { + progress.log(` checking checked state of "${selector}"`); + return this._scheduleRerunnableTask(progress, info.world, task); + }, this._page._timeoutSettings.timeout(options)); + } + async hover(controller: ProgressController, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { return controller.run(async progress => { return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._hover(progress, options))); diff --git a/test/elementhandle-convenience.spec.ts b/test/elementhandle-convenience.spec.ts index 5a9243140620a..39d2954d0f1ad 100644 --- a/test/elementhandle-convenience.spec.ts +++ b/test/elementhandle-convenience.spec.ts @@ -207,3 +207,15 @@ it('isEditable should work', async ({ page }) => { expect(await textarea.isEditable()).toBe(false); expect(await page.isEditable('textarea')).toBe(false); }); + +it('isChecked should work', async ({page}) => { + await page.setContent(`
Not a checkbox
`); + const handle = await page.$('input'); + expect(await handle.isChecked()).toBe(true); + expect(await page.isChecked('input')).toBe(true); + await handle.evaluate(input => input.checked = false); + expect(await handle.isChecked()).toBe(false); + expect(await page.isChecked('input')).toBe(false); + const error = await page.isChecked('div').catch(e => e); + expect(error.message).toContain('Not a checkbox or radio button'); +}); diff --git a/test/page-check.spec.ts b/test/page-check.spec.ts index 6b1dbd88450e0..0a49009330508 100644 --- a/test/page-check.spec.ts +++ b/test/page-check.spec.ts @@ -92,3 +92,9 @@ it('should check the box by aria role', async ({page}) => { await page.check('div'); expect(await page.evaluate(() => window['checkbox'].getAttribute('aria-checked'))).toBe('true'); }); + +it('should throw when not a checkbox', async ({page}) => { + await page.setContent(`
Check me
`); + const error = await page.check('div').catch(e => e); + expect(error.message).toContain('Not a checkbox or radio button'); +}); diff --git a/types/types.d.ts b/types/types.d.ts index e594c100340e5..27e2a686ad370 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -2033,6 +2033,22 @@ export interface Page { timeout?: number; }): Promise; + /** + * Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://github.com/microsoft/playwright/blob/master/docs/selectors.md#working-with-selectors) for more details. + * @param options + */ + isChecked(selector: string, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(…)](https://github.com/microsoft/playwright/blob/master/docs/api.md#browsercontextsetdefaulttimeout) + * or [page.setDefaultTimeout(…)](https://github.com/microsoft/playwright/blob/master/docs/api.md#pagesetdefaulttimeout) + * methods. + */ + timeout?: number; + }): Promise; + /** * Indicates that the page has been closed. */ @@ -3991,6 +4007,22 @@ export interface Frame { timeout?: number; }): Promise; + /** + * Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://github.com/microsoft/playwright/blob/master/docs/selectors.md#working-with-selectors) for more details. + * @param options + */ + isChecked(selector: string, options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(…)](https://github.com/microsoft/playwright/blob/master/docs/api.md#browsercontextsetdefaulttimeout) + * or [page.setDefaultTimeout(…)](https://github.com/microsoft/playwright/blob/master/docs/api.md#pagesetdefaulttimeout) + * methods. + */ + timeout?: number; + }): Promise; + /** * Returns `true` if the frame has been detached, or `false` otherwise. */ @@ -5857,6 +5889,11 @@ export interface ElementHandle extends JSHandle { */ innerText(): Promise; + /** + * Returns whether the element is checked. Throws if the element is not a checkbox or radio input. + */ + isChecked(): Promise; + /** * Returns whether the element is disabled, the opposite of [enabled](https://github.com/microsoft/playwright/blob/master/docs/actionability.md#enabled). */