feat: semantic locators for dragSlider/moveCursorTo and a new setSliderValue - #5705
Closed
DavertMik wants to merge 2 commits into
Closed
feat: semantic locators for dragSlider/moveCursorTo and a new setSliderValue#5705DavertMik wants to merge 2 commits into
DavertMik wants to merge 2 commits into
Conversation
Both actions resolved their locator with _locateElement, which wraps the
argument as new Locator(locator, 'css'), so a human readable string was
treated as a CSS selector. There was no findFields / findClickable / role
step anywhere in either chain, and both failed on plain HTML:
dragSlider('Legacy volume', 40) could not find an <input type="range">
labelled by <label for>, and moveCursorTo('Show details') could not find
a <button>.
dragSlider now resolves a fuzzy string through getByRole('slider', {name,
exact: true}), then findFields, then a non-exact role match, then CSS or
XPath. Role-exact runs first because Locator.field.labelContains ends in
unrestricted aria-labelledby branches, which return the wrapping container
ahead of the input for widgets that mirror their label onto both.
moveCursorTo resolves through findClickable, the same path click uses, in
both the plain and the context branch. Non-fuzzy locators short-circuit to
the existing lookup, so CSS, XPath, strict and role locators behave exactly
as before.
Puppeteer and WebDriver have no getByRole, so they use a new
Locator.slider.byLabel XPath covering aria-label, <label for>,
aria-labelledby and a wrapping <label>, restricted to input[type=range]
and [role=slider].
Also fixes the WebDriver drag itself: it resolved the locator three times
and then moved the pointer by the element's page coordinates from wherever
the pointer happened to be, since performActions starts its input source at
the origin. That only landed on the slider when the slider spanned the page.
It now resolves once and moves to the element origin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi
There was no way to set a slider to a value. fillField can not be delegated to fill() for every slider: a range input that a component library collapses to 0x0 is not visible, so fill() times out, and a <span role="slider"> is not an input at all. Pixel dragging can not reach a zero size element either. focus() plus Home/End and arrow keys works for all of them. setSliderValue(locator, value) reads aria-valuemin, aria-valuemax, aria-valuenow and step, falling back to the min, max and value attributes of a native range and then to 0 and 100. It focuses the element, moves to whichever of the current value, Home or End is cheapest, presses the arrow key for the axis given by aria-orientation the required number of times, and asserts the value actually reached the target. Widgets that are not form fields expose no step, so the increment is measured by pressing one arrow key and reading the delta. Starting from the current value rather than always from Home matters over the wire: on WebDriver the same case went from 23.7s to 0.9s. An unreachable or out of range value raises a message naming the step and the nearest reachable values instead of timing out. dragSlider on an element with no bounding box no longer throws; it applies offsetX as that many keyboard steps and says so in the debug log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi
Contributor
Author
|
not great implementation |
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.
Problem
dragSliderandmoveCursorToresolved their locator with_locateElement, which wraps the argument asnew Locator(locator, 'css'). A human-readable string was therefore treated as a CSS selector — there was nofindFields/findClickable/ role step anywhere in either chain. This is not a component-library problem, it fails on plain HTML:docs/webapi/dragSlider.mustachehas always claimed "For fuzzy locators, fields are matched by label text, the name attribute, CSS, and XPath". It was not true until now.Separately, there was no way to set a slider to a value.
fill()cannot be delegated to for every slider — measured:fill('60')<input type=range>0×0<span role="slider">focus()+Home/End+ arrow keys is the one mechanism that reaches all three, including elements with no bounding box.Commit 1 — semantic locators for
dragSliderandmoveCursorTodragSliderresolves a fuzzy string through a newfindSlider:getByRole('slider', { name, exact: true })→findFields→ non-exact role → CSS/XPath. Role-exact runs first on purpose:Locator.field.labelContainsends in unrestricted.//*[@aria-labelledby…]branches, so on Base UIfindFieldsreturns[div[role=group], input]and the container wins.moveCursorToresolves throughfindClickable, the same pathclickuses, in both the plain and thecontextbranch.getByRole, so they use a newLocator.slider.byLabelXPath (aria-label,<label for>,aria-labelledby, wrapping<label>) restricted toinput[type=range] | *[role=slider], thenfindFields, then CSS/XPath.{role:…}) short-circuit to the existing lookup, so nothing changes for them.WebDriver drag fix, found on the way.
dragSliderresolved the locator three times (moveCursorTo+ twograbElementBoundingRectcalls) and then issuedpointerMovewithorigin: 'pointer'and the element's page coordinates as the offset.performActionsstarts its input source at(0,0), so this only landed on the slider when the slider was full-width — which is exactly what the existing fixture is. It now resolves once and moves to the element origin. The existing#dragSlidertest is unchanged and still green.Commit 2 —
setSliderValueReads
aria-valuemin/aria-valuemax/aria-valuenow/step, falling back to themin/max/valueattributes of a native range and then to0/100. Focuses the element, moves to the cheapest of{ current value, Home, End }and pressesArrowRight/ArrowLeft(orArrowUp/ArrowDownwhenaria-orientation="vertical") the required number of times, then assertsaria-valuenowactually reached the target.Widgets that are not form fields expose no
step— Radix's<span role="slider">has none — so the increment is measured by pressing one arrow key and reading the delta. Starting from the current value rather than always fromHomematters over the wire: on WebDriver the Radix case went from 23.7s to 0.9s.Errors say what happened rather than timing out:
dragSlideron an element with no bounding box no longer throws; it appliesoffsetXas that many keyboard steps and says so in the debug log.Needs your call —
setSliderValuevs extendingfillFieldThe plan left this open and asked me to implement (a) and flag (b) for review:
I.setSliderValue(locator, value)— what this PR ships. Explicit, honest about the mechanism, no overloading.fillFieldto acceptrole=slidertargets —I.fillField('Volume', 60)reads more naturally, but it silently switches mechanism fromfill()to key presses, andfillFieldalready branches into rich-text and (with feat: fillField works with role=combobox widgets #5701) combobox handling; a third special case makes it harder to reason about.Say the word and I will move it to
fillField— nothing else in the PR depends on the name.Second judgement call: on a
0×0sliderdragSlider(locator, 40)moves 40 steps, not 40 pixels, since there are no pixels to move along. If you would rather it threw and pointed atsetSliderValue, that is a two-line change.Notes for review
moveCursorTonow goes throughselectElement, so it honoursstrictmode and theelementIndexstep option likeclickdoes. It used to takeels[0]unconditionally.aria-labelonSlider.Thumb. A<label for>onSlider.Roottargets a<span>, which is not labelable, so the thumb has no accessible name and no framework can synthesise one. Documented indocs/basics.md.0×0range input is a CSS outcome, not a library constant. An unsizedSlider.Thumbcollapses and takes the input with it (fill()times out, dragging is impossible); a sized thumb yields a 16×16 clipped input that both reach. The fixture covers both, so the keyboard path is exercised against a genuinely zero-size element.window.__ready). Base UI is pinned to@base-ui/react@1.8.0— the package was renamed from@base-ui-components/react, which feat: fillField works with role=combobox widgets #5701 pins.typings/types.d.tsis gitignored and regenerated by the dtslint job;npm run defandnpm run dtslintpass locally.Tests
test/helper/webapi.js, so all three helpers run them:#dragSlider - semantic locators,#moveCursorTo - semantic locators,#setSliderValue. Every case asserts observable state —aria-valuenow/valuereaching the target, the tooltip text appearing — never "no error thrown".Fixtures:
test/data/app/view/form/slider/{native,radix,baseui}.phpandtest/data/app/view/form/hover/radix.php, plus two named triggers added totest/data/app/view/form/hover.php.Local: 25/25 slider and hover cases on Playwright, Puppeteer and WebDriver (Selenium 4.27 in Docker); 770 unit tests; lint clean;
npm run defandnpm run dtslintclean. Full helper belts: WebDriver 408/408. Playwright and Puppeteer each showed a handful of failures unrelated to this change - the sharedtest/data/app/dbrace and cookie/3rd-party-network cases - which reproduce on4.xwith this branch stashed, and pass when the affected suites are run on their own.🤖 Generated with Claude Code
https://claude.ai/code/session_01TcwzSXPnfaig8nBZD2Vxfi