fix(check): support all ARIA roles that could be aria-checked (#18304)

Fixes #18193.
This commit is contained in:
Dmitry Gozman 2022-10-25 06:11:11 -07:00 committed by GitHub
parent 329b3eadb4
commit 3cd64e1449
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 47 additions and 23 deletions

View File

@ -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}"`);
}

View File

@ -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'];

View File

@ -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(`<div role=${role} aria-checked=true>I am checked</div>`);
const locator = page.locator('div');
await expect(locator).toBeChecked();
});
}
});
});
test.describe('toBeEditable', () => {

View File

@ -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(`<div role='checkbox' id='checkbox'>CHECKBOX</div>
<script>
checkbox.addEventListener('click', () => checkbox.setAttribute('aria-checked', 'true'));
</script>`);
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(`<div role='${role}' id='checkbox'>CHECKBOX</div>
<script>
checkbox.addEventListener('click', () => checkbox.setAttribute('aria-checked', 'true'));
</script>`);
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(`<div role='checkbox' id='checkbox' aria-checked="true">CHECKBOX</div>
<script>
checkbox.addEventListener('click', () => checkbox.setAttribute('aria-checked', 'false'));
</script>`);
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(`<div role='${role}' id='checkbox' aria-checked="true">CHECKBOX</div>
<script>
checkbox.addEventListener('click', () => checkbox.setAttribute('aria-checked', 'false'));
</script>`);
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(`<div role=button>Check me</div>`);
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(`<div role='button'><input type='checkbox'></div>`);
await page.check('input');