diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index c4cabf115..91f90bffd 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -50,7 +50,6 @@ let defaultSelectorEnginesInitialized = false
const popupStore = new Popup()
const consoleLogStore = new Console()
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
-const checkableRoles = ['checkbox', 'radio', 'switch']
import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js'
import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'
@@ -4389,17 +4388,6 @@ async function findCheckable(locator, context) {
return findElements.call(this, contextEl, matchedLocator)
}
- for (const exact of [true, false]) {
- for (const role of checkableRoles) {
- try {
- const roleEls = await contextEl.getByRole(role, { name: matchedLocator.value, exact }).all()
- if (roleEls.length) return roleEls
- } catch (err) {
- // getByRole not supported or failed
- }
- }
- }
-
const literal = xpathLocator.literal(matchedLocator.value)
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
if (els.length) {
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index 7e147e757..3d1f7a02e 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -64,7 +64,6 @@ function wrapError(e) {
let perfTiming
const popupStore = new Popup()
const consoleLogStore = new Console()
-const checkableRoles = ['checkbox', 'radio', 'switch']
/**
* ## Configuration
@@ -3196,19 +3195,8 @@ async function findCheckable(locator, context) {
return findElements.call(this, contextEl, matchedLocator)
}
- // Try ARIA selector for accessible name
- let els
- for (const role of checkableRoles) {
- try {
- els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`)
- if (els.length) return els
- } catch (err) {
- // ARIA selector not supported or failed
- }
- }
-
const literal = xpathLocator.literal(matchedLocator.value)
- els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
+ let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
if (els.length) {
return els
}
@@ -3217,6 +3205,14 @@ async function findCheckable(locator, context) {
return els
}
+ // Try ARIA selector for accessible name
+ try {
+ els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
+ if (els.length) return els
+ } catch (err) {
+ // ARIA selector not supported or failed
+ }
+
return findElements.call(this, contextEl, matchedLocator.value)
}
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index a52d85d9d..1d12b7f41 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -3248,14 +3248,6 @@ async function findCheckable(locator, locateFn) {
if (locator.isRole()) return locateFn(locator, true)
if (!locator.isFuzzy()) return locateFn(locator, true)
- // Try ARIA selector for accessible name
- try {
- els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`))
- if (els.length) return els
- } catch (e) {
- // ARIA selector not supported or failed
- }
-
const literal = xpathLocator.literal(locator.value)
els = await locateFn(Locator.checkable.byText(literal))
if (els.length) return els
@@ -3263,23 +3255,17 @@ async function findCheckable(locator, locateFn) {
els = await locateFn(Locator.checkable.byName(literal))
if (els.length) return els
+ // Try ARIA selector for accessible name
+ try {
+ els = await locateFn(`aria/${locator.value}`)
+ if (els.length) return els
+ } catch (e) {
+ // ARIA selector not supported or failed
+ }
+
return await locateFn(locator.value) // by css or xpath
}
-async function keepCheckable(els) {
- if (!els || !els.length) return []
-
- const checkable = await this.browser.execute(function () {
- return Array.prototype.slice.call(arguments).map(function (el) {
- if (!el) return false
- const role = el.getAttribute('role')
- if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1
- return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')
- })
- }, ...els)
-
- return els.filter((el, index) => checkable[index])
-}
function withStrictLocator(locator) {
locator = new Locator(locator)
diff --git a/lib/locator.js b/lib/locator.js
index 6a47b71d2..913fdd2dd 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -649,6 +649,9 @@ Locator.field = {
]),
}
+const checkable = `self::input[@type = 'checkbox' or @type = 'radio'] or @role = 'checkbox' or @role = 'radio' or @role = 'switch'`
+const visibleCheckable = `.//*[${checkable}][not(@aria-hidden = 'true')]`
+
Locator.checkable = {
/**
* @param {string} literal
@@ -656,8 +659,10 @@ Locator.checkable = {
*/
byText: literal =>
xpathLocator.combine([
- `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
- `.//label[contains(normalize-space(string(.)), ${literal})]//input[@type = 'radio' or @type = 'checkbox']`,
+ `${visibleCheckable}[(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
+ `.//label[contains(normalize-space(string(.)), ${literal})]//*[${checkable}][not(@aria-hidden = 'true')]`,
+ `${visibleCheckable}[@aria-labelledby = //*[@id][contains(normalize-space(string(.)), ${literal})]/@id]`,
+ `${visibleCheckable}[@aria-label = ${literal}]`,
]),
/**
diff --git a/test/unit/locator_test.js b/test/unit/locator_test.js
index 9c59216ae..706ddc4ca 100644
--- a/test/unit/locator_test.js
+++ b/test/unit/locator_test.js
@@ -808,4 +808,47 @@ describe('Locator', () => {
expect(items[0].getAttribute('id')).to.eql('rename')
})
})
+
+ describe('Locator.checkable.byText', () => {
+ const select = (xml, literal) => {
+ const doc = new DOMParser().parseFromString(xml, 'text/xml')
+ return xpath.select(Locator.checkable.byText(literal), xpath.select1('//root', doc))
+ }
+
+ it('matches a native input labelled by label[for]', () => {
+ const nodes = select('', "'I Agree'")
+ expect(nodes).to.have.length(1)
+ expect(nodes[0].getAttribute('id')).to.eql('a')
+ })
+
+ it('matches a role=checkbox labelled by label[for]', () => {
+ const nodes = select('', "'Accept terms'")
+ expect(nodes).to.have.length(1)
+ expect(nodes[0].tagName).to.eql('button')
+ })
+
+ it('matches a role=switch labelled by aria-labelledby', () => {
+ const nodes = select('', "'Airplane mode'")
+ expect(nodes).to.have.length(1)
+ expect(nodes[0].getAttribute('role')).to.eql('switch')
+ })
+
+ it('matches a role=radio named by aria-label', () => {
+ const nodes = select('', "'Compact'")
+ expect(nodes).to.have.length(1)
+ expect(nodes[0].getAttribute('role')).to.eql('radio')
+ })
+
+ it('resolves the visible control, not the aria-hidden input the label points at', () => {
+ const xml =
+ '' +
+ '' +
+ '' +
+ '' +
+ ''
+ const nodes = select(xml, "'Accept terms'")
+ expect(nodes).to.have.length(1)
+ expect(nodes[0].getAttribute('id')).to.eql('visible')
+ })
+ })
})