Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
22 changes: 9 additions & 13 deletions lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function wrapError(e) {
let perfTiming
const popupStore = new Popup()
const consoleLogStore = new Console()
const checkableRoles = ['checkbox', 'radio', 'switch']

/**
* ## Configuration
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}

Expand Down
30 changes: 8 additions & 22 deletions lib/helper/WebDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3248,38 +3248,24 @@ 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

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)
Expand Down
9 changes: 7 additions & 2 deletions lib/locator.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,15 +649,20 @@ 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
* @returns {string}
*/
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}]`,
]),

/**
Expand Down
43 changes: 43 additions & 0 deletions test/unit/locator_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<root><input type="checkbox" id="a"/><label for="a">I Agree</label></root>', "'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('<root><button role="checkbox" id="a"></button><label for="a">Accept terms</label></root>', "'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('<root><span role="switch" aria-labelledby="l"></span><label id="l">Airplane mode</label></root>', "'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('<root><span role="radio" aria-label="Compact"></span></root>', "'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 =
'<root>' +
'<span role="checkbox" aria-labelledby="l" id="visible"></span>' +
'<input type="checkbox" id="mirror" aria-hidden="true"/>' +
'<label for="mirror" id="l">Accept terms</label>' +
'</root>'
const nodes = select(xml, "'Accept terms'")
expect(nodes).to.have.length(1)
expect(nodes[0].getAttribute('id')).to.eql('visible')
})
})
})
Loading