diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts index eaa330a0e5..7aa22e6f0c 100644 --- a/packages/playwright-core/src/server/injected/selectorGenerator.ts +++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts @@ -15,7 +15,7 @@ */ import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from '../../utils/isomorphic/stringUtils'; -import { closestCrossShadow, isInsideScope, parentElementOrShadowHost } from './domUtils'; +import { closestCrossShadow, isElementVisible, isInsideScope, parentElementOrShadowHost } from './domUtils'; import type { InjectedScript } from './injectedScript'; import { getAriaRole, getElementAccessibleName, beginAriaCaches, endAriaCaches } from './roleUtils'; import { elementText, getElementLabels } from './selectorUtils'; @@ -89,7 +89,12 @@ export function generateSelector(injectedScript: InjectedScript, targetElement: } selectors = [joinTokens(targetTokens)]; } else { - targetElement = closestCrossShadow(targetElement, 'button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]', options.root) || targetElement; + // Note: this matches InjectedScript.retarget(). + if (!targetElement.matches('input,textarea,select') && !(targetElement as any).isContentEditable) { + const interactiveParent = closestCrossShadow(targetElement, 'button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]', options.root); + if (interactiveParent && isElementVisible(interactiveParent)) + targetElement = interactiveParent; + } if (options.multiple) { const withText = generateSelectorFor(injectedScript, targetElement, options); const withoutText = generateSelectorFor(injectedScript, targetElement, { ...options, noText: true }); diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts index 3d7370dd0d..e9df1afc6e 100644 --- a/tests/library/selector-generator.spec.ts +++ b/tests/library/selector-generator.spec.ts @@ -33,13 +33,22 @@ it.describe('selector generator', () => { }); it('should prefer button over inner span', async ({ page }) => { - await page.setContent(``); - expect(await generate(page, 'button')).toBe('#clickme'); + await page.setContent(``); + expect(await generate(page, 'span')).toBe('internal:role=button[name="text"i]'); }); it('should prefer role=button over inner span', async ({ page }) => { - await page.setContent(`
`); - expect(await generate(page, 'div')).toBe('internal:role=button'); + await page.setContent(`
text
`); + expect(await generate(page, 'span')).toBe('internal:role=button[name="text"i]'); + }); + + it('should not prefer zero-sized button over inner span', async ({ page }) => { + await page.setContent(` + + `); + expect(await generate(page, 'span')).toBe('internal:text="text"i'); }); it('should generate text and normalize whitespace', async ({ page }) => {