In the Selenium series we’ll solve problems that are commonly faced when automating web testing with Selenium. Automating html5 input type=range element is one of the tricky ones to get right. Here we provide a method that works across browsers using Selenium’s Python bindings.
CHALLENGE
The input range element is one of the “new” input types. It consists of a slider and a thumb and it can have minimum and maximum values. We want to simulate the user dragging the slider thumb to a desired value.
ITERATIONS TO THE SOLUTION
1. THE VALUE OF ANY INPUT ELEMENT CAN BE CHANGED VIA JAVASCRIPT EXECUTOR.
def set_range(el, val):
driver.execute_javascript("arguments[0].value = arguments[1];", el, val);
view rawrange1.py hosted with ❤ by GitHubUnfortunately the javascript emulation does not trigger the oninput/onchange handlers associated with the element. Therefore the application under test behaves unlike in a real world scenario.
2. CALCULATE THE CLICK POSITION BY USING THE ELEMENT’S WIDTH AND USE ACTIONCHAINS TO MOVE THE MOUSE TO THE POSITION AND CLICK AT IT.
def set_range(el, val):
minval = float(el.get_attribute("min") or 0)
maxval = float(el.get_attribute("max") or 100)
v = max(0, min(1, (float(val) - minval) / (maxval - minval)))
width = el.size["width"]
target = float(width) * v
ac = ActionChains(driver)
ac.move_to_element_with_offset(el, target, 1)
ac.click()
ac.perform()
view rawrange2.py hosted with ❤ by GitHubThis will get you close, but as you’ll see it is not stable – on some ranges with some values, this will miss the desired value by some offset. It has probably something to do with the browser’s margins and paddings and the input element’s shadow DOM’s graphical representation. Certainly this is not stable across browsers.
3. CLICK ON THE CALCULATED POSITION AND PERFORM A BINARY SEARCH UNTIL THE DESIRED VALUE IS ACHIEVED
A stable solution will check that after the slider position has been changed, the current value is checked and the thumb possibly adjusted until the value is what we want.
The problem with this approach is that if you miss the target value by a pixel margin that is less than the width of the slider thumb, a follow-up click nearby won’t change the slider state because a click on the thumb won’t change the slider value. Therefore a binary search with clicks won’t be stable.
4. CLICK AND HOLD THE RANGE THUMB AND PERFORM A BINARY SEARCH BY DRAGGING THE SLIDER THUMB
The problem of clicking on the thumb will be resolved by dragging the thumb – this is what real users would usually do. This solution will work except when the first click/release cycle on the slider thumb hits the goal and no adjustment by dragging is needed. In that case, the slider value won’t change and an oninput event won’t happen. This may sometimes be what is wanted, but generally if an input is changed in a test case, you’d usually expect the event handlers to trigger.
5. A STABLE CROSS-BROSSER SOLUTION
In the final stable solution we’ll perform a binary search by dragging. Before starting the search, we’ll drag the slider from minimum to maximum value. This will ensure we’ll generate an oninput event on the input range element. Here’s the code.
def set_range(el, val):
# The adjustment helper to drag the slider thumb
def adjust(deltax):
if deltax < 0:
deltax = int(math.floor(min(-1, deltax)))
else:
deltax = int(math.ceil(max(1, deltax)))
ac = ActionChains(driver)
ac.click_and_hold(None)
ac.move_by_offset(deltax, 0)
ac.release(None)
ac.perform()
minval = float(el.get_attribute("min") or 0)
maxval = float(el.get_attribute("max") or 100)
v = max(0, min(1, (float(val) - minval) / (maxval - minval)))
width = el.size["width"]
target = float(width) * v
ac = ActionChains(driver)
# drag from min to max value, to ensure oninput event
ac.move_to_element_with_offset(el, 0, 1)
ac.click_and_hold()
ac.move_by_offset(width, 0)
# drag to the calculated position
ac.move_to_element_with_offset(el, target, 1)
ac.release()
ac.perform()
# perform a binary search and adjust the slider thumb until the value matches
while True:
curval = el.get_attribute("value")
if float(curval) == float(val):
return True
prev_guess = target
if float(curval) < float(val):
minguess = target
target += (maxguess - target) / 2
else:
maxguess = target
target = minguess + (target - minguess) / 2
deltax = target-prev_guess
if abs(deltax) < 0.5
break # cannot find a way, fallback to javascript.
time.sleep(0.1) # Don't consume CPU too much
adjust(deltax)
# Finally, if the binary search algoritm fails to achieve the final value
# we'll revert to the javascript method so at least the value will be changed
# even though the browser events wont' be triggered.
# Fallback
driver.execute_script("arguments[0].value=arguments[1];", el, val)
curval = el.get_attribute("value")
if float(curval) == float(val):
return True
else:
raise Exception("Can't set value %f for the element." % val)
view rawrange3.py hosted with ❤ by GitHub
How would you solve this?