refactor(locator): find ARIA checkables in Locator, not in each helper - #5708
Open
DavertMik wants to merge 1 commit into
Open
refactor(locator): find ARIA checkables in Locator, not in each helper#5708DavertMik wants to merge 1 commit into
DavertMik wants to merge 1 commit into
Conversation
Locator.checkable already knows how to find a control by its label. It just
hard-coded the tag. Headless component libraries express the same semantics
with a role, so generalise the tag test to a tag-or-role test and skip
aria-hidden elements.
self::input[@type='checkbox' or @type='radio']
or @ROLE='checkbox' or @ROLE='radio' or @ROLE='switch'
This replaces the three per-helper implementations added in #5704 — a
getByRole loop in Playwright, a role-scoped ::-p-aria loop in Puppeteer, and a
hoisted aria/ lookup plus keepCheckable filter in WebDriver — with one XPath
predicate that all three helpers inherit, since they all call
Locator.checkable.byText. Puppeteer's and WebDriver's ARIA fallbacks return to
their original position after the XPath.
An XPath predicate is role-scoped by construction, so there is no precision
trade-off to manage: a heading sharing the label text cannot match, and the
per-helper filtering that guarded against it is no longer needed.
The aria-hidden guard is what skips the hidden mirror input that libraries
render and point <label for> at, so the visible control is resolved instead.
Behaviour is unchanged for native controls: on existing fixtures the generated
XPath returns an identical node set.
Verified against real radix-ui@1.6.7 and @base-ui/react@1.8.0 components:
checkOption and seeCheckboxIsChecked work for Checkbox, Switch, Radio Group and
Checkbox Group on both libraries, standalone and inside a form.
Playwright 53 passing, Puppeteer 49 passing, WebDriver 41 passing (3 pending),
unit 815 + 81 locator, lint clean. Net -25 lines of library code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #5704, which I wrote and which you were right to find inelegant. It solved the problem three times — once per helper — and left the concept out of
Locator, where it belongs.The idea
Locator.checkablealready knows how to find a control by its label:<label for>→@id, nesting inside the label,@aria-label,@aria-labelledby. Those mechanisms are correct. The only thing wrong is that they hard-code the tag. Headless component libraries express the same semantics with a role.Generalise the tag test to a tag-or-role test, skip
aria-hidden. Nothing else. Mechanisms 3 and 4 already existed onLocator.field; this brings them toLocator.checkable, where they were missing.What it removes
getByRoleloop over 3 roles × exact/substring::-p-arialoop +checkableRolesaria/+keepCheckablefilterLocatorPuppeteer's and WebDriver's ARIA fallbacks return to their original position after the XPath. Net −25 lines of library code, no new public methods, and nothing in the diff names a component library.
An XPath predicate is role-scoped by construction, so the precision problem #5704 had to solve per-helper — a heading sharing the label text winning over the control — cannot arise, and the filtering that guarded against it is gone.
The
not(@aria-hidden = 'true')guard is what skips the hidden mirror input these libraries render and point<label for>at, so the visible control is resolved instead. Same guard #5703 uses forLocator.field.Verification
Against real
radix-ui@1.6.7and@base-ui/react@1.8.0, standalone and inside a<form>—checkOption/seeCheckboxIsCheckedon Checkbox, Switch, Radio Group, Checkbox Group work on both libraries.locator_test.jsincluding 5 new cases pinning the predicate#5704's own behaviour-level tests are kept unchanged and still pass; they were good, and they are what proves this is a refactor rather than a rewrite.Two things I checked and did not claim
checkOptiononmenuitemcheckboxalready works on merged4.x. Behaviour is identical before and after this change.proceedSeeCheckbox→findFields, which isLocator.field's territory (see fix(locator): restrict loose field branches to field-like elements #5703), notfindCheckable's. Skip left in place.Scope
Deliberately excluded:
aria-pressedtoggles (a toggle button is not a checkable, andI.click('Bold')already works),role=menuitemcheckbox, and sliders. None of them need new public API to state, and none were the ask.🤖 Generated with Claude Code
https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi