diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 1377e73647..ca1b5c7fad 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../isomorphic/cssParser'; import { generateSelector } from './selectorGenerator'; import type * as channels from '@protocol/channels'; import { Highlight } from './highlight'; -import { getAriaDisabled, getAriaRole, getElementAccessibleName } from './roleUtils'; +import { getAriaCheckedStrict, getAriaDisabled, getAriaRole, getElementAccessibleName } from './roleUtils'; import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils'; import { asLocator } from '../isomorphic/locatorGenerators'; import type { Language } from '../isomorphic/locatorGenerators'; @@ -609,16 +609,11 @@ export class InjectedScript { return !disabled && editable; if (state === 'checked' || state === 'unchecked') { - if (['checkbox', 'radio'].includes(element.getAttribute('role') || '')) { - const result = element.getAttribute('aria-checked') === 'true'; - return state === 'checked' ? result : !result; - } - if (element.nodeName !== 'INPUT') + const need = state === 'checked'; + const checked = getAriaCheckedStrict(element); + if (checked === 'error') throw this.createStacklessError('Not a checkbox or radio button'); - if (!['radio', 'checkbox'].includes((element as HTMLInputElement).type.toLowerCase())) - throw this.createStacklessError('Not a checkbox or radio button'); - const result = (element as HTMLInputElement).checked; - return state === 'checked' ? result : !result; + return need === checked; } throw this.createStacklessError(`Unexpected element state "${state}"`); } diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index bbaeec6a23..7c4ea0a245 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -635,6 +635,10 @@ export function getAriaSelected(element: Element): boolean { export const kAriaCheckedRoles = ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem']; export function getAriaChecked(element: Element): boolean | 'mixed' { + const result = getAriaCheckedStrict(element); + return result === 'error' ? false : result; +} +export function getAriaCheckedStrict(element: Element): boolean | 'mixed' | 'error' { // https://www.w3.org/TR/wai-aria-1.2/#aria-checked // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (element.tagName === 'INPUT' && (element as HTMLInputElement).indeterminate) @@ -647,8 +651,9 @@ export function getAriaChecked(element: Element): boolean | 'mixed' { return true; if (checked === 'mixed') return 'mixed'; + return false; } - return false; + return 'error'; } export const kAriaPressedRoles = ['button']; diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index 03343dd181..01968a5cde 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -76,6 +76,16 @@ test.describe('toBeChecked', () => { expect(error.message).toContain(`expect.toBeChecked with timeout 1000ms`); expect(error.message).toContain('waiting for "locator(\'input2\')"'); }); + + test('with role', async ({ page }) => { + for (const role of ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem']) { + await test.step(`role=${role}`, async () => { + await page.setContent(`