From 8ad3bc7ff3a0fc0d134eaab7a9a6eead13f59c5b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 21 Nov 2022 14:13:51 -0800 Subject: [PATCH] fix(role selector): expanded=false does not match elements without aria-expanded (#18929) Fixes #18920. --- .../src/server/injected/roleSelectorEngine.ts | 5 ++ .../src/server/injected/roleUtils.ts | 14 ++++-- tests/page/selectors-role.spec.ts | 49 +++++++++++++------ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts index 3e4d1ac5e9c51..a3fdb5c3ba363 100644 --- a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts +++ b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts @@ -72,6 +72,11 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string) { validateSupportedRole(attr.name, kAriaExpandedRoles, role); validateSupportedValues(attr, [true, false]); validateSupportedOp(attr, ['', '=']); + if (attr.op === '') { + // Do not match "none" in "treeitem[expanded]". + attr.op = '='; + attr.value = true; + } break; } case 'level': { diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 7c4ea0a2450b9..9d9c1be95c73e 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -670,14 +670,20 @@ export function getAriaPressed(element: Element): boolean | 'mixed' { } export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch']; -export function getAriaExpanded(element: Element): boolean { +export function getAriaExpanded(element: Element): boolean | 'none' { // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (element.tagName === 'DETAILS') return (element as HTMLDetailsElement).open; - if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) - return getAriaBoolean(element.getAttribute('aria-expanded')) === true; - return false; + if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { + const expanded = element.getAttribute('aria-expanded'); + if (expanded === null) + return 'none'; + if (expanded === 'true') + return true; + return false; + } + return 'none'; } export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem']; diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts index 0ea07f17c56d3..253afd9475f5c 100644 --- a/tests/page/selectors-role.spec.ts +++ b/tests/page/selectors-role.spec.ts @@ -169,26 +169,42 @@ test('should support pressed', async ({ page }) => { test('should support expanded', async ({ page }) => { await page.setContent(` - - - +
Hi
+
Hello
+ `); - expect(await page.locator(`role=button[expanded]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ - ``, + + expect(await page.locator('role=treeitem').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hi
`, + `
Hello
`, + ``, ]); - expect(await page.locator(`role=button[expanded=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ - ``, + expect(await page.getByRole('treeitem').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hi
`, + `
Hello
`, + ``, ]); - expect(await page.getByRole('button', { expanded: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ - ``, + + expect(await page.locator(`role=treeitem[expanded]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hello
`, ]); - expect(await page.locator(`role=button[expanded=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ - ``, - ``, + expect(await page.locator(`role=treeitem[expanded=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hello
`, ]); - expect(await page.getByRole('button', { expanded: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ - ``, - ``, + expect(await page.getByRole('treeitem', { expanded: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hello
`, + ]); + + expect(await page.locator(`role=treeitem[expanded=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + expect(await page.getByRole('treeitem', { expanded: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + + // Workaround for expanded="none". + expect(await page.locator(`[role=treeitem]:not([aria-expanded])`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
Hi
`, ]); }); @@ -403,4 +419,7 @@ test('errors', async ({ page }) => { const e7 = await page.$('role=button[name]').catch(e => e); expect(e7.message).toContain(`"name" attribute must have a value`); + + const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e); + expect(e8.message).toContain(`"expanded" must be one of true, false`); });