diff --git a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts index b647f81b8e..293c10aa5f 100644 --- a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts +++ b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts @@ -147,8 +147,7 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo return; } if (options.name !== undefined) { - // Always normalize whitespace in the accessible name. - const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, !!options.includeHidden)); + const accessibleName = getElementAccessibleName(element, !!options.includeHidden); if (typeof options.name === 'string') options.name = normalizeWhiteSpace(options.name); // internal:role assumes that [name="foo"i] also means substring. diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 50564fa31f..16743fd607 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -16,6 +16,7 @@ import type { AriaRole } from '@isomorphic/ariaSnapshot'; import { closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils'; +import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils'; function hasExplicitAccessibleName(e: Element) { return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby'); @@ -420,6 +421,8 @@ export function getElementAccessibleName(element: Element, includeHidden: boolea })); } + // Note: we always normalize whitespace in the accessible name to make it easier to work with. + accessibleName = normalizeWhiteSpace(accessibleName); cache?.set(element, accessibleName); } return accessibleName; @@ -452,6 +455,8 @@ export function getElementAccessibleDescription(element: Element, includeHidden: accessibleDescription = asFlatString(element.getAttribute('title') || ''); } + // Note: we always normalize whitespace in the accessible description to make it easier to work with. + accessibleDescription = normalizeWhiteSpace(accessibleDescription); cache?.set(element, accessibleDescription); } return accessibleDescription; diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index a02680ce86..a94cf67c2f 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -475,24 +475,9 @@ test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ pa expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' }); }); -test('should not include hidden pseudo into accessible name', async ({ page }) => { - await page.setContent(` - - - hello -
hello
-
- `); - expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' }); +test('should normalize accessible name', async ({ page }) => { + await page.setContent(``); + expect.soft(await getNameAndRole(page, 'button')).toEqual({ role: 'button', name: 'foo bar baz' }); }); function toArray(x: any): any[] { diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 2242a55378..f99ac12376 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -431,6 +431,9 @@ test('toHaveAccessibleName', async ({ page }) => { await expect(page.locator('div')).toHaveAccessibleName(/ell\w/); await expect(page.locator('div')).not.toHaveAccessibleName(/hello/); await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true }); + + await page.setContent(``); + await expect(page.locator('button')).toHaveAccessibleName('foo bar baz'); }); test('toHaveAccessibleDescription', async ({ page }) => { @@ -443,6 +446,12 @@ test('toHaveAccessibleDescription', async ({ page }) => { await expect(page.locator('div')).toHaveAccessibleDescription(/ell\w/); await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/); await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true }); + + await page.setContent(` +
+ foo bar\nbaz + `); + await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz'); }); test('toHaveRole', async ({ page }) => { diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index a7937e9be5..8d2149ef9c 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -479,6 +479,14 @@ it('should escape yaml text in text nodes', async ({ page }) => { `); }); +it('should normalize accessible name', async ({ page }) => { + await page.setContent(``); + + await checkAndMatchSnapshot(page.locator('body'), ` + - button "foo bar baz" + `); +}); + it('should handle long strings', async ({ page }) => { const s = 'a'.repeat(10000); await page.setContent(`