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(`
I am checked
`); + const locator = page.locator('div'); + await expect(locator).toBeChecked(); + }); + } + }); }); test.describe('toBeEditable', () => { diff --git a/tests/page/page-check.spec.ts b/tests/page/page-check.spec.ts index 8e9ec4c681..01b00ddc55 100644 --- a/tests/page/page-check.spec.ts +++ b/tests/page/page-check.spec.ts @@ -69,21 +69,29 @@ it('should uncheck radio by aria role', async ({ page }) => { }); it('should check the box by aria role', async ({ page }) => { - await page.setContent(` - `); - await page.check('div'); - expect(await page.evaluate(() => window['checkbox'].getAttribute('aria-checked'))).toBe('true'); + for (const role of ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem']) { + await it.step(`role=${role}`, async () => { + await page.setContent(`
CHECKBOX
+ `); + await page.check('div'); + expect(await page.evaluate(() => window['checkbox'].getAttribute('aria-checked'))).toBe('true'); + }); + } }); it('should uncheck the box by aria role', async ({ page }) => { - await page.setContent(` - `); - await page.uncheck('div'); - expect(await page.evaluate(() => window['checkbox'].getAttribute('aria-checked'))).toBe('false'); + for (const role of ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem']) { + await it.step(`role=${role}`, async () => { + await page.setContent(`
CHECKBOX
+ `); + await page.uncheck('div'); + expect(await page.evaluate(() => window['checkbox'].getAttribute('aria-checked'))).toBe('false'); + }); + } }); it('should throw when not a checkbox', async ({ page }) => { @@ -92,6 +100,12 @@ it('should throw when not a checkbox', async ({ page }) => { expect(error.message).toContain('Not a checkbox or radio button'); }); +it('should throw when not a checkbox 2', 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'); +}); + it('should check the box inside a button', async ({ page }) => { await page.setContent(`
`); await page.check('input');