testa_appium_driver 0.1.21 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +23 -0
  3. data/.gitignore +17 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +6 -0
  6. data/Gemfile.lock +1 -1
  7. data/bin/console +17 -0
  8. data/bin/setup +8 -0
  9. data/lib/testa_appium_driver/android/class_selectors.rb +438 -0
  10. data/lib/testa_appium_driver/android/driver.rb +71 -0
  11. data/lib/testa_appium_driver/android/locator/attributes.rb +115 -0
  12. data/lib/testa_appium_driver/android/locator.rb +142 -0
  13. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +62 -0
  14. data/lib/testa_appium_driver/android/selenium_element.rb +12 -0
  15. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  16. data/lib/testa_appium_driver/common/constants.rb +38 -0
  17. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  18. data/lib/testa_appium_driver/common/helpers.rb +272 -0
  19. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +390 -0
  20. data/lib/testa_appium_driver/common/locator.rb +640 -0
  21. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  22. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +380 -0
  23. data/lib/testa_appium_driver/common/scroll_actions.rb +275 -0
  24. data/lib/testa_appium_driver/common/selenium_element.rb +19 -0
  25. data/lib/testa_appium_driver/driver.rb +338 -0
  26. data/lib/testa_appium_driver/ios/driver.rb +49 -0
  27. data/lib/testa_appium_driver/ios/locator/attributes.rb +89 -0
  28. data/lib/testa_appium_driver/ios/locator.rb +73 -0
  29. data/lib/testa_appium_driver/ios/selenium_element.rb +8 -0
  30. data/lib/testa_appium_driver/ios/type_selectors.rb +201 -0
  31. data/lib/testa_appium_driver.rb +4 -0
  32. data/testa_appium_driver.gemspec +6 -2
  33. metadata +39 -11
  34. data/appium-driver.iml +0 -79
@@ -0,0 +1,115 @@
1
+ module TestaAppiumDriver
2
+ module Attributes
3
+
4
+ #noinspection RubyNilAnalysis
5
+ def testa_attribute(name, *args)
6
+ if self.instance_of?(::Selenium::WebDriver::Element) || self.instance_of?(::Appium::Core::Element)
7
+ @driver = get_driver # does not get correct driver
8
+ elements = self
9
+ else
10
+ elements = execute(*args)
11
+ raise "Element not found" if elements.nil?
12
+ end
13
+
14
+ if elements.kind_of?(::Selenium::WebDriver::Element) || elements.kind_of?(::Appium::Core::Element)
15
+ r = elements.send(:attribute, name.to_s)
16
+ r = TestaAppiumDriver::Bounds.from_android(r, @driver) if name.to_s == "bounds"
17
+ else
18
+ r = elements.map { |e| e.send(:attribute, name.to_s) }
19
+ r.map! { |b| TestaAppiumDriver::Bounds.from_android(b, @driver) } if name.to_s == "bounds"
20
+ end
21
+ r
22
+ end
23
+
24
+ def text(*args)
25
+ testa_attribute("text", *args)
26
+ end
27
+
28
+ def package(*args)
29
+ testa_attribute("package", *args)
30
+ end
31
+
32
+ def class_name(*args)
33
+ testa_attribute("className", *args)
34
+ end
35
+
36
+ def checkable?(*args)
37
+ testa_attribute("checkable", *args).to_s == "true"
38
+ end
39
+
40
+ def checked?(*args)
41
+ testa_attribute("checked", *args).to_s == "true"
42
+ end
43
+
44
+ def clickable?(*args)
45
+ testa_attribute("clickable", *args).to_s == "true"
46
+ end
47
+
48
+ def desc(*args)
49
+ testa_attribute("contentDescription", *args)
50
+ end
51
+
52
+ def enabled?(*args)
53
+ testa_attribute("enabled", *args).to_s == "true"
54
+ end
55
+
56
+ def focusable?(*args)
57
+ testa_attribute("focusable", *args).to_s == "true"
58
+ end
59
+
60
+ def focused?(*args)
61
+ testa_attribute("focused", *args).to_s == "true"
62
+ end
63
+
64
+ def long_clickable?(*args)
65
+ testa_attribute("longClickable", *args).to_s == "true"
66
+ end
67
+
68
+ def password?(*args)
69
+ testa_attribute("password", *args).to_s == "true"
70
+ end
71
+
72
+ def id(*args)
73
+ testa_attribute("resourceId", *args)
74
+ end
75
+
76
+ def scrollable?(*args)
77
+ testa_attribute("scrollable", *args).to_s == "true"
78
+ end
79
+
80
+ def selected?(*args)
81
+ testa_attribute("selected", *args).to_s == "true"
82
+ end
83
+
84
+ def displayed?(*args)
85
+ testa_attribute("displayed", *args).to_s == "true"
86
+ end
87
+
88
+ def selection_start(*args)
89
+ testa_attribute("selection-start", *args)
90
+ end
91
+
92
+ def selection_end(*args)
93
+ testa_attribute("selection-end", *args)
94
+ end
95
+
96
+ def bounds(*args)
97
+ testa_attribute("bounds", *args)
98
+ end
99
+ end
100
+
101
+ class Locator
102
+
103
+ # element index in parent element, starts from 0
104
+ #noinspection RubyNilAnalysis,RubyYardReturnMatch
105
+ # @return [Integer, nil] index of element
106
+ def index(*args)
107
+ raise "Index not supported for uiautomator strategy" if @strategy == FIND_STRATEGY_UIAUTOMATOR
108
+ this = execute(*args)
109
+ children = self.dup.parent.children.execute
110
+ index = children.index(this)
111
+ raise "Index not found" if index.nil?
112
+ index.to_i
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,142 @@
1
+ require_relative 'locator/attributes'
2
+
3
+ module TestaAppiumDriver
4
+ #noinspection RubyTooManyInstanceVariablesInspection
5
+ class Locator
6
+ attr_accessor :closing_parenthesis
7
+ include ClassSelectors
8
+ include Attributes
9
+
10
+ def init(params, selectors, single)
11
+ @closing_parenthesis = 0
12
+
13
+
14
+ @ui_selector = hash_to_uiautomator(selectors, single)
15
+
16
+ if is_scrollable_selector?(selectors, single)
17
+ if selectors[:class] == "android.widget.HorizontalScrollView"
18
+ @scroll_orientation = :horizontal
19
+ else
20
+ @scroll_orientation = :vertical
21
+ end
22
+
23
+ if !params[:top].nil? || !params[:bottom].nil? || !params[:right].nil? || !params[:left].nil?
24
+ @scroll_deadzone = {}
25
+ @scroll_deadzone[:top] = params[:top].to_f unless params[:top].nil?
26
+ @scroll_deadzone[:bottom] = params[:bottom].to_f unless params[:bottom].nil?
27
+ @scroll_deadzone[:right] = params[:right].to_f unless params[:right].nil?
28
+ @scroll_deadzone[:left] = params[:left].to_f unless params[:left].nil?
29
+ end
30
+
31
+ params[:scrollable_locator] = self.dup
32
+ end
33
+
34
+ @scrollable_locator = params[:scrollable_locator] if params[:scrollable_locator]
35
+ end
36
+
37
+
38
+ # resolve selector which will be used for finding element
39
+ def strategies_and_selectors
40
+ ss = []
41
+ if @can_use_id_strategy
42
+ ss.push({"#{FIND_STRATEGY_ID}": @can_use_id_strategy})
43
+ end
44
+ if @strategy.nil? || @strategy == FIND_STRATEGY_UIAUTOMATOR
45
+ ss.push({"#{FIND_STRATEGY_UIAUTOMATOR}": ui_selector})
46
+ end
47
+
48
+ if @strategy.nil? || @strategy == FIND_STRATEGY_XPATH
49
+ ss.push({"#{FIND_STRATEGY_XPATH}": @xpath_selector})
50
+ end
51
+
52
+ if @strategy == FIND_STRATEGY_IMAGE
53
+ ss.push({"#{FIND_STRATEGY_IMAGE}": @image_selector})
54
+ end
55
+ ss
56
+ end
57
+
58
+
59
+ # @param [Boolean] include_semicolon should the semicolon be included at the end
60
+ # @return ui_selector for uiautomator find strategy
61
+ def ui_selector(include_semicolon = true)
62
+ @ui_selector + ")" * @closing_parenthesis + (include_semicolon ? ";" : "");
63
+ end
64
+
65
+ def ui_selector=(value)
66
+ @ui_selector = value
67
+ end
68
+
69
+
70
+
71
+
72
+ # @return [TestaAppiumDriver::Locator]
73
+ def from_parent(selectors = {})
74
+ raise "Cannot add from_parent selector to array" unless @single
75
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_UIAUTOMATOR, "from_parent") if @strategy != FIND_STRATEGY_UIAUTOMATOR
76
+
77
+ locator = self.dup
78
+ locator.strategy = FIND_STRATEGY_UIAUTOMATOR
79
+ locator.strategy_reason = "from_parent"
80
+ locator.closing_parenthesis += 1
81
+ locator.ui_selector = "#{locator.ui_selector}.fromParent(#{hash_to_uiautomator(selectors)}"
82
+ locator
83
+ end
84
+
85
+
86
+ # @return [Locator] new child locator element
87
+ def add_child_selector(params)
88
+ params, selectors = extract_selectors_from_params(params)
89
+ single = params[:single]
90
+ raise "Cannot add child selector to Array" if single && !@single
91
+
92
+ locator = self.dup
93
+ locator.can_use_id_strategy = false
94
+ if (@strategy.nil? && !single) || @strategy == FIND_STRATEGY_XPATH
95
+ locator.strategy = FIND_STRATEGY_XPATH
96
+ locator.strategy_reason = "multiple child selector"
97
+ add_xpath_child_selectors(locator, selectors, single)
98
+ elsif @strategy == FIND_STRATEGY_UIAUTOMATOR
99
+ locator = add_uiautomator_child_selector(locator, selectors, single)
100
+ elsif @strategy == FIND_STRATEGY_IMAGE
101
+ locator = add_image_child_selector(locator, selectors, single)
102
+ else
103
+ # both paths are valid
104
+ add_xpath_child_selectors(locator, selectors, single)
105
+ locator = add_uiautomator_child_selector(locator, selectors, single)
106
+ end
107
+
108
+ if is_scrollable_selector?(selectors, single)
109
+ locator.scrollable_locator = locator
110
+ if selectors[:class] == "android.widget.HorizontalScrollView"
111
+ locator.scrollable_locator.scroll_orientation = :horizontal
112
+ else
113
+ locator.scrollable_locator.scroll_orientation = :vertical
114
+ end
115
+ end
116
+
117
+ locator.last_selector_adjacent = false
118
+ locator
119
+ end
120
+
121
+
122
+ private
123
+ def add_uiautomator_child_selector(locator, selectors, single)
124
+ if locator.single && !single
125
+ # current locator stays single, the child locator looks for multiple
126
+ params = selectors.merge({single: single, scrollable_locator: locator.scrollable_locator})
127
+ params[:default_find_strategy] = locator.default_find_strategy
128
+ params[:default_scroll_strategy] = locator.default_scroll_strategy
129
+ Locator.new(@driver, self, params)
130
+ else
131
+ locator.single = true
132
+ locator.ui_selector = "#{locator.ui_selector(false)}.childSelector(#{hash_to_uiautomator(selectors, single)})"
133
+ locator
134
+ end
135
+ end
136
+
137
+ def add_image_child_selector(locator, selectors, single)
138
+ params = selectors.merge({single: single, scrollable_locator: locator.scrollable_locator})
139
+ Locator.new(@driver, self, params)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,62 @@
1
+ module TestaAppiumDriver
2
+ #noinspection RubyInstanceMethodNamingConvention
3
+ class ScrollActions
4
+ private
5
+
6
+ def uiautomator_scroll_to_start_or_end(type)
7
+
8
+ scrollable_selector = @scrollable.ui_selector(false)
9
+ orientation = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
10
+ scroll_command = type == :start ? ".scrollToBeginning(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})" : ".scrollToEnd(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})"
11
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{scroll_command};"
12
+ begin
13
+ puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
14
+ @driver.find_element(uiautomator: cmd)
15
+ rescue
16
+ # Ignored
17
+ end
18
+
19
+
20
+ end
21
+
22
+
23
+ def uiautomator_scroll_to
24
+ raise "UiAutomator scroll cannot work with specified direction" unless @direction.nil?
25
+
26
+ scrollable_selector = @scrollable.ui_selector(false)
27
+ element_selector = @locator.ui_selector(false)
28
+ orientation_command = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
29
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation_command}.scrollIntoView(#{element_selector});"
30
+ begin
31
+ puts "Scroll execute[uiautomator_scroll_to]: #{cmd}"
32
+ @driver.find_element(uiautomator: cmd)
33
+ rescue
34
+ # Ignored
35
+ ensure
36
+ end
37
+ end
38
+
39
+
40
+ def uiautomator_page_or_fling(type, direction)
41
+ scrollable_selector = @scrollable.ui_selector(false)
42
+ orientation = direction == :up || direction == :down ? ".setAsVerticalList()" : ".setAsHorizontalList()"
43
+ if type == SCROLL_ACTION_TYPE_SCROLL
44
+ direction_command = direction == :down || direction == :right ? ".scrollForward()" : ".scrollBackward()"
45
+ elsif type == SCROLL_ACTION_TYPE_FLING
46
+ direction_command = direction == :down || direction == :right ? ".flingForward()" : ".flingBackward()"
47
+ else
48
+ raise "Unknown scroll action type #{type}"
49
+ end
50
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{direction_command};"
51
+ begin
52
+ puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
53
+ @driver.find_element(uiautomator: cmd)
54
+ rescue
55
+ # Ignored
56
+ end
57
+ end
58
+
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,12 @@
1
+ module ::Appium
2
+ module Core
3
+ class Element
4
+ include TestaAppiumDriver::ClassSelectors
5
+ include TestaAppiumDriver::Attributes
6
+
7
+ def parent
8
+ self.find_element(xpath: "./..")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ module TestaAppiumDriver
5
+ class Bounds
6
+
7
+ attr_reader :width
8
+ attr_reader :height
9
+ attr_reader :offset
10
+
11
+
12
+ # @param top_left [Coordinates]
13
+ # @param bottom_right [Coordinates]
14
+ # @param window_width [Integer]
15
+ # @param window_height [Integer]
16
+ def initialize(top_left, bottom_right, window_width, window_height)
17
+ @top_left = top_left
18
+ @bottom_right = bottom_right
19
+ @width = bottom_right.x - top_left.x
20
+ @height = bottom_right.y - top_left.y
21
+ @offset = Offset.new(self, window_width, window_height)
22
+ @center = TestaAppiumDriver::Coordinates.new(@top_left.x + @width/2, @top_left.y + @height / 2)
23
+ end
24
+
25
+ def as_json
26
+ {
27
+ width: @width,
28
+ height: @height,
29
+ top_left: @top_left.as_json,
30
+ bottom_right: @bottom_right.as_json,
31
+ offset: @offset.as_json
32
+ }
33
+ end
34
+
35
+ # @return [TestaAppiumDriver::Offset]
36
+ def offset
37
+ @offset
38
+ end
39
+
40
+ # @return [TestaAppiumDriver::Coordinates]
41
+ def top_left
42
+ @top_left
43
+ end
44
+ # @return [TestaAppiumDriver::Coordinates]
45
+ def bottom_right
46
+ @bottom_right
47
+ end
48
+
49
+ # @return [TestaAppiumDriver::Coordinates]
50
+ def center
51
+ @center
52
+ end
53
+
54
+ def to_s
55
+ JSON.dump(as_json)
56
+ end
57
+
58
+ # @param bounds [String] bounds that driver.attribute("bounds") return
59
+ # @param driver [TestaAppiumDriver::Driver]
60
+ def self.from_android(bounds, driver)
61
+ matches = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/)
62
+ raise "Unexpected bounds: #{bounds}" unless matches
63
+
64
+ captures = matches.captures
65
+ top_left = Coordinates.new(captures[0], captures[1])
66
+ bottom_right = Coordinates.new(captures[2], captures[3])
67
+ ws = driver.window_size
68
+ window_width = ws.width.to_i
69
+ window_height = ws.height.to_i
70
+ Bounds.new(top_left, bottom_right, window_width, window_height)
71
+ end
72
+
73
+ def self.from_ios(rect, driver)
74
+ rect = JSON.parse(rect)
75
+ top_left = Coordinates.new(rect["x"], rect["y"])
76
+ bottom_right = Coordinates.new(top_left.x + rect["width"].to_i, top_left.y + rect["height"].to_i)
77
+ ws = driver.window_size
78
+ window_width = ws.width.to_i
79
+ window_height = ws.height.to_i
80
+ Bounds.new(top_left, bottom_right, window_width, window_height)
81
+ end
82
+ end
83
+
84
+ #noinspection ALL
85
+ class Coordinates
86
+ def initialize(x, y)
87
+ @x = x.to_i
88
+ @y = y.to_i
89
+ end
90
+
91
+ def as_json
92
+ {
93
+ x: @x,
94
+ y: @y
95
+ }
96
+ end
97
+
98
+
99
+ # @return [Integer]
100
+ def x
101
+ @x
102
+ end
103
+
104
+ # @return [Integer]
105
+ def y
106
+ @y
107
+ end
108
+ end
109
+
110
+
111
+ class Offset
112
+ def initialize(bounds, window_width, window_height)
113
+ @top = bounds.top_left.y
114
+ @right = window_width - bounds.bottom_right.x
115
+ @bottom = window_height - bounds.bottom_right.y
116
+ @left = bounds.top_left.x
117
+ end
118
+
119
+ def as_json
120
+ {
121
+ top: @top,
122
+ right: @right,
123
+ bottom: @bottom,
124
+ left: @left
125
+ }
126
+ end
127
+
128
+
129
+ # @return [Integer]
130
+ def top
131
+ @top
132
+ end
133
+
134
+ # @return [Integer]
135
+ def right
136
+ @right
137
+ end
138
+
139
+ # @return [Integer]
140
+ def bottom
141
+ @bottom
142
+ end
143
+
144
+ # @return [Integer]
145
+ def left
146
+ @left
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #noinspection ALL
4
+ module TestaAppiumDriver
5
+ FIND_STRATEGY_UIAUTOMATOR = :uiautomator
6
+ FIND_STRATEGY_XPATH = :xpath
7
+ FIND_STRATEGY_ID = :id
8
+ FIND_STRATEGY_NAME = :name
9
+ FIND_STRATEGY_IMAGE = :image
10
+ FIND_STRATEGY_CLASS_CHAIN = :class_chain
11
+
12
+ SCROLL_STRATEGY_UIAUTOMATOR = :uiautomator
13
+ SCROLL_STRATEGY_W3C = :w3c
14
+
15
+
16
+ SCROLL_CORRECTION_W3C = 30
17
+ SCROLL_ALIGNMENT_THRESHOLD = 25
18
+
19
+ SCROLL_ACTION_TYPE_SCROLL = :scroll
20
+ SCROLL_ACTION_TYPE_FLING = :fling
21
+ SCROLL_ACTION_TYPE_DRAG = :drag
22
+
23
+
24
+ DEFAULT_UIAUTOMATOR_MAX_SWIPES = 20
25
+
26
+ DEFAULT_ANDROID_FIND_STRATEGY = FIND_STRATEGY_UIAUTOMATOR
27
+ #DEFAULT_ANDROID_SCROLL_STRATEGY = SCROLL_STRATEGY_UIAUTOMATOR
28
+ DEFAULT_ANDROID_SCROLL_STRATEGY = SCROLL_STRATEGY_W3C
29
+
30
+
31
+ DEFAULT_IOS_FIND_STRATEGY = FIND_STRATEGY_XPATH
32
+ DEFAULT_IOS_SCROLL_STRATEGY = SCROLL_STRATEGY_W3C
33
+
34
+ DEFAULT_W3C_MAX_SCROLLS = 7
35
+
36
+ EXISTS_WAIT = 0.5
37
+ LONG_TAP_DURATION = 1.5
38
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestaAppiumDriver
4
+ class StrategyMixException < Exception
5
+ def initialize(strategy, strategy_reason, mixed_strategy, mixed_reason)
6
+
7
+ # example: parent is only available in xpath strategy and cannot be used with from_element which is only available in uiautomator strategy
8
+ msg = "strategy mix exception: '#{strategy_reason}' is only available in #{strategy} strategy and cannot be used with '#{mixed_reason}' which is only available in #{mixed_strategy} strategy"
9
+ super(msg)
10
+ end
11
+ end
12
+ end