Skip to content

Commit c4e7fed

Browse files
DavertMikclaude
andcommitted
fix: role-restrict the hoisted ARIA checkable lookup
Moving the accessible-name lookup above the label XPath fixed Base UI but widened the net: `aria/…` and `::-p-aria(…)` match on name alone, so a heading sharing a checkbox label's text now won on document order where `byText` previously reached the input. Measured on the new collision fixture, both returned `[H2, INPUT#terms-box]`. Puppeteer loops the three checkable roles as `::-p-aria([name][role])`, following the buildRoleSelector convention already in the file. `::-p-aria` matches names exactly and case-sensitively, so one pass per role is enough; a name that cannot be parsed falls through to the XPath as before. WebDriver has no attribute filter on `aria/`, so its results are post-filtered to `input[type=checkbox|radio]` and `[role=checkbox|radio|switch]` in a single `browser.execute` round trip regardless of match count, using the `execute(fn, ...elements)` form already used in the file. Both now resolve `[INPUT#terms-box]`, matching Playwright's role-scoped pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi
1 parent 5747dcc commit c4e7fed

4 files changed

Lines changed: 47 additions & 6 deletions

File tree

‎lib/helper/Puppeteer.js‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function wrapError(e) {
6464
let perfTiming
6565
const popupStore = new Popup()
6666
const consoleLogStore = new Console()
67+
const checkableRoles = ['checkbox', 'radio', 'switch']
6768

6869
/**
6970
* ## Configuration
@@ -3194,11 +3195,13 @@ async function findCheckable(locator, context) {
31943195

31953196
// Try ARIA selector for accessible name
31963197
let els
3197-
try {
3198-
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
3199-
if (els.length) return els
3200-
} catch (err) {
3201-
// ARIA selector not supported or failed
3198+
for (const role of checkableRoles) {
3199+
try {
3200+
els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`)
3201+
if (els.length) return els
3202+
} catch (err) {
3203+
// ARIA selector not supported or failed
3204+
}
32023205
}
32033206

32043207
const literal = xpathLocator.literal(matchedLocator.value)

‎lib/helper/WebDriver.js‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3247,7 +3247,7 @@ async function findCheckable(locator, locateFn) {
32473247

32483248
// Try ARIA selector for accessible name
32493249
try {
3250-
els = await locateFn(`aria/${locator.value}`)
3250+
els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`))
32513251
if (els.length) return els
32523252
} catch (e) {
32533253
// ARIA selector not supported or failed
@@ -3263,6 +3263,21 @@ async function findCheckable(locator, locateFn) {
32633263
return await locateFn(locator.value) // by css or xpath
32643264
}
32653265

3266+
async function keepCheckable(els) {
3267+
if (!els || !els.length) return []
3268+
3269+
const checkable = await this.browser.execute(function () {
3270+
return Array.prototype.slice.call(arguments).map(function (el) {
3271+
if (!el) return false
3272+
const role = el.getAttribute('role')
3273+
if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1
3274+
return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')
3275+
})
3276+
}, ...els)
3277+
3278+
return els.filter((el, index) => checkable[index])
3279+
}
3280+
32663281
function withStrictLocator(locator) {
32673282
locator = new Locator(locator)
32683283
return locator.simplify()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Checkable name collision</title>
6+
</head>
7+
<body>
8+
<h2>Accept terms</h2>
9+
<form action="/form/complex" method="POST">
10+
<label for="terms-box">Accept terms</label>
11+
<input type="checkbox" id="terms-box" name="terms" value="agree" />
12+
<input type="submit" value="Submit" />
13+
</form>
14+
</body>
15+
</html>

‎test/helper/webapi.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,14 @@ export function tests() {
583583
await I.click('Submit')
584584
assert.equal(formContents('terms'), 'agree')
585585
})
586+
587+
it('ignores a non-control sharing the accessible name', async () => {
588+
await I.amOnPage('/form/checkable/collision')
589+
await I.dontSeeCheckboxIsChecked('#terms-box')
590+
591+
await I.checkOption('Accept terms')
592+
await I.seeCheckboxIsChecked('#terms-box')
593+
})
586594
})
587595

588596
describe('#selectOption', () => {

0 commit comments

Comments
 (0)