From 8b018f6b4154782f7dc525e3df94d7374b001880 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 6 Oct 2022 13:35:10 -0800 Subject: [PATCH] chore: make role name case-insensitive (#17888) --- packages/playwright-core/src/client/locator.ts | 4 ++-- .../src/server/injected/selectorGenerator.ts | 8 ++++---- .../playwright-core/src/utils/isomorphic/stringUtils.ts | 4 ++-- tests/page/selectors-role.spec.ts | 4 ++++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 5e8b25ec43..b40e2b6677 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -396,7 +396,7 @@ export function setTestIdAttribute(attributeName: string) { function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string { if (!isString(text)) return `internal:attr=[${attrName}=${text}]`; - return `internal:attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`; + return `internal:attr=[${attrName}=${escapeForAttributeSelector(text, options?.exact || false)}]`; } export function getByTestIdSelector(testId: string): string { @@ -439,7 +439,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st if (options.level !== undefined) props.push(['level', String(options.level)]); if (options.name !== undefined) - props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name) : String(options.name)]); + props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]); if (options.pressed !== undefined) props.push(['pressed', String(options.pressed)]); return `role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`; diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts index 7f3e000cef..3de7a8d95f 100644 --- a/packages/playwright-core/src/server/injected/selectorGenerator.ts +++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts @@ -148,7 +148,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces const candidates: SelectorToken[] = []; if (element.getAttribute('data-testid')) - candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!)}]`, score: 1 }); + candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!, true)}]`, score: 1 }); for (const attr of ['data-test-id', 'data-test']) { if (element.getAttribute(attr)) @@ -158,7 +158,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { const input = element as HTMLInputElement | HTMLTextAreaElement; if (input.placeholder) - candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder)}]`, score: 3 }); + candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: 3 }); const label = input.labels?.[0]; if (label) { const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim(); @@ -170,13 +170,13 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces if (ariaRole) { const ariaName = getElementAccessibleName(element, false, accessibleNameCache); if (ariaName) - candidates.push({ engine: 'role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName)}]`, score: 3 }); + candidates.push({ engine: 'role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: 3 }); else candidates.push({ engine: 'role', selector: ariaRole, score: 150 }); } if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) - candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!)}]`, score: 10 }); + candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!, true)}]`, score: 10 }); if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName)) candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 }); diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 831e7bbb81..f060eeb658 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -72,10 +72,10 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas return text; } -export function escapeForAttributeSelector(value: string): string { +export function escapeForAttributeSelector(value: string, exact: boolean): string { // TODO: this should actually be // cssEscape(value).replace(/\\ /g, ' ') // However, our attribute selectors do not conform to CSS parsing spec, // so we escape them differently. - return `"${value.replace(/["]/g, '\\"')}"`; + return `"${value.replace(/["]/g, '\\"')}"${exact ? '' : 'i'}`; } diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts index 8df22dc7d5..b6a428593c 100644 --- a/tests/page/selectors-role.spec.ts +++ b/tests/page/selectors-role.spec.ts @@ -360,6 +360,10 @@ test('should support name', async ({ page }) => { `
`, ``, ]); + expect(await page.getByRole('button', { name: 'hello', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + `
`, + ``, + ]); expect(await page.locator(`role=button[name=Hello]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ `
`,