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
69 changes: 60 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,15 +334,16 @@ Roles are ALWAYS defined at the begining of the cases. You have to write always
7. [press](#press)
8. [click_and_hold](#click_and_hold)
9. [swipe_up/swipe_down](#swipe_up/swipe_down)
10. [swipe_on_element](#swipe_on_element)
11. [swipe_elements](#swipe_elements)
12. [swipe_coord](#swipe_coord)
13. [click_coord](#click_coord)
14. [clipboard](#clipboard)
15. [handle_ios_alert](#handle_ios_alert)
16. [notifications](#notifications)
17. [back](#back)
18. [update_settings](#update_settings)
10. [scroll_until_element_visible](#scroll_until_element_visible)
11. [swipe_on_element](#swipe_on_element)
12. [swipe_elements](#swipe_elements)
13. [swipe_coord](#swipe_coord)
14. [click_coord](#click_coord)
15. [clipboard](#clipboard)
16. [handle_ios_alert](#handle_ios_alert)
17. [notifications](#notifications)
18. [back](#back)
19. [update_settings](#update_settings)

## API

Expand Down Expand Up @@ -789,6 +790,56 @@ It works simillar as click, but it holds the pressing. The labels and options th
Id: //some/path (Element from where to start the swipe)
NoRaise: false/true (Default - false -> will rise error on fail)

### <a id="scroll_until_element_visible"></a>scroll_until_element_visible

Scroll vertically (up or down) until the specified element is visible on the screen. This function accepts many optional parameters, but the simplest form is as follows:

- Type: scroll_until_element_visible
Role: role1 (Optional. if not specified will use the first one defined in the case Roles)
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path

Set a specific background element to scroll on (the scroll target). If not specified, the scroll target is set to the entire visible window.

- Type: scroll_until_element_visible
Role: role1
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path
ScrollTarget:
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path
RecheckAfterScrolls: 2 (Sometimes the scroll target dimensions may change, e.g. in mobile browsers, the URL bar may auto-hide. Setting this value recalculates the scroll target dimensions after the specified number of swipes)

Customize the swipe action:

- Type: scroll_until_element_visible
Role: role1
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path
SwipeAction:
OffsetFractionX: 0.3 (Fraction of the scroll target width where the swipe action will execute. Default 0.5, i.e. the midpoint of the scroll target width)
StartFractionY: 0.2 (Fraction of the scroll target height where the swipe action will start. Default 0.7)
EndFractionY: 0.8 (Fraction of the scroll target height where the swipe action will end. Default 0.3)
SwipeSpeedMultiplier: 1.2 (Speed multiplier to apply to the default scroll speed)
SwipePauseDuration: 0.5 (Time in seconds to wait after every swipe action. Default 0.2 for iOS, otherwise 0.1)

Set the scrolling timeout:

- Type: scroll_until_element_visible
Role: role1
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path
ScrollTimeout: 90 (In seconds - default is 60)

Instruct the target element to be scrolled into full view. This means that once the element is visible, one additional swipe will be executed from the element location, to either the top or the bottom of the scroll target (depending on the scroll direction configured in the swipe action).

- Type: scroll_until_element_visible
Role: role1
Strategy: id/css/xpath/uiautomator/class_chain/...
Id: //some/path
FullView: true/false (Default false)
FullViewOffsetY: 150 (Optional - offset in pixels added to either the top or bottom of the scroll target, only when executing the full view swipe action. This can be used to ensure that the element is not scrolled outside the scroll target dimensions. If this is set, FullView is assumed to be true and can be omitted)

### <a id="swipe_elements"></a>swipe_elements

- Type: swipe_elements
Expand Down
15 changes: 15 additions & 0 deletions examples/tests/cases/case_swipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ SwipeElementsTest:
Id: '//android.view.View[1]'
- Type: sleep
Time: 5

ScrollUntilElementVisibleTest:
Vars:
USER: localAndroid
Roles:
- Role: $AND_CLI_USER$
App: PlayStore
Actions:
- Type: click
Role: $AND_CLI_USER$
Strategy: uiautomator
Id: text("Top charts")
- Type: scroll_until_element_visible
Strategy: uiautomator
Id: text("25")
108 changes: 108 additions & 0 deletions lib/core/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,114 @@ def swipe_down(action)
.perform
end

# Scrolls vertically until the specified element is visible or a timeout is reached
# Accepts:
# Strategy
# Id
# ScrollTarget
# Strategy
# Id
# RecheckAfterScrolls
# SwipeAction
# OffsetFractionX
# StartFractionY
# EndFractionY
# SwipeSpeedMultiplier
# SwipePauseDuration
# ScrollTimeout
# FullView
# FullViewOffsetY
def scroll_until_element_visible(action)
# the element will be checked in a loop, so its search should be fast and never fail
original_noraise = action["NoRaise"]
action["NoRaise"] = true
action["Time"] = 0.5
# configure swipe action properties
x_frac = 0.5
y_start_frac = 0.7
y_end_frac = 0.3
scroll_mul = @platform == "iOS" ? 3 : 1.5
scroll_pause = @platform == "iOS" ? 0.2 : 0.1
if action.key?("SwipeAction")
sw_action = action["SwipeAction"]
x_frac = convert_value(sw_action["OffsetFractionX"]).to_f if sw_action.key?("OffsetFractionX")
y_start_frac = convert_value(sw_action["StartFractionY"]).to_f if sw_action.key?("StartFractionY")
y_end_frac = convert_value(sw_action["EndFractionY"]).to_f if sw_action.key?("EndFractionY")
scroll_mul /= convert_value(sw_action["SwipeSpeedMultiplier"]).to_f if sw_action.key?("SwipeSpeedMultiplier")
scroll_pause = convert_value(sw_action["SwipePauseDuration"]).to_f if sw_action.key?("SwipePauseDuration")
end
scroll_timeout = action.key?("ScrollTimeout") ? convert_value(action["ScrollTimeout"]).to_f : 60
# calculate the exact coordinates for swiping,
# depending on whether a specific element to swipe on is provided
if action.key?("ScrollTarget")
recheck_after_scrolls = nil
if action["ScrollTarget"].key?("RecheckAfterScrolls")
recheck_after_scrolls = convert_value(action["ScrollTarget"]["RecheckAfterScrolls"]).to_i
end
bg_el = wait_for(action["ScrollTarget"])
y_top = bg_el.location.y
y_bottom = bg_el.location.y + bg_el.size.height
x_point = bg_el.location.x + (bg_el.size.width * x_frac)
y_start = y_top + (bg_el.size.height * y_start_frac)
y_end = y_top + (bg_el.size.height * y_end_frac)
else
screen_size = @driver.window_size
y_top = 0
y_bottom = screen_size.height
x_point = screen_size.width * x_frac
y_start = screen_size.height * y_start_frac
y_end = screen_size.height * y_end_frac
end
# configure FullView parameters
# if FullViewOffsetY is provided, assume that FullView is requested
action["FullView"] = true if action.key?("FullViewOffsetY")
y_fullview_offset = action.key?("FullViewOffsetY") ? convert_value(action["FullViewOffsetY"]).to_i : 0

# start the scrolling/checking loop
scrolls = 0
start = Time.now
while (Time.now - start) < scroll_timeout
el = wait_for(action)
if el&.displayed?
return unless action.key?("FullView") && convert_value(action["FullView"]) == "true"
el_y_loc = el.location.y # save position so it is not retrieved every time
# save the target point depending on scroll direction
full_view_target = y_start_frac > y_end_frac ? y_top + y_fullview_offset : y_bottom + y_fullview_offset
# if the following condition is true, the full view swipe has already been executed, so we can return
return if y_end == full_view_target
# if element is visible but not in full view, change the scroll endpoints
if el_y_loc > y_top && el_y_loc < y_bottom
y_end = full_view_target
y_start = el_y_loc
end
end
# element not displayed or not in full view - execute swipe action
duration = (y_start - y_end).abs * scroll_mul / 1000.0
log_info("#{@role}: Scrolling from [#{x_point}, #{y_start}] to [#{x_point}, #{y_end}] for #{duration}s")
@driver.action
.move_to_location(x_point, y_start)
.pointer_down(:left)
.move_to_location(x_point, y_end, duration: duration)
.pause(device: @driver.action.key_inputs[0], duration: scroll_pause)
.release
.perform
scrolls += 1
# recalculate the scroll target properties if the recheck variable is set
next unless action.key?("ScrollTarget") && scrolls == recheck_after_scrolls
bg_el = wait_for(action["ScrollTarget"])
y_top = bg_el.location.y
y_bottom = bg_el.location.y + bg_el.size.height
x_point = bg_el.location.x + (bg_el.size.width * x_frac)
y_start = y_top + (bg_el.size.height * y_start_frac)
y_end = y_top + (bg_el.size.height * y_end_frac)
end
# raise error if timeout exceeded
return if original_noraise
path = take_error_screenshot
raise "\nRole #{@role}: Element '#{action["Id"]}' is not visible after scrolling for #{scroll_timeout} " +
"seconds\nError Screenshot: #{path}"
end

def swipe_elements(action)
# swipe from element1 to element2
el1 = wait_for(action["Element1"])
Expand Down