testa_appium_driver 0.1.21 → 0.1.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) 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/bin/console +17 -0
  7. data/bin/setup +8 -0
  8. data/lib/testa_appium_driver/android/class_selectors.rb +438 -0
  9. data/lib/testa_appium_driver/android/driver.rb +71 -0
  10. data/lib/testa_appium_driver/android/locator/attributes.rb +115 -0
  11. data/lib/testa_appium_driver/android/locator.rb +142 -0
  12. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +62 -0
  13. data/lib/testa_appium_driver/android/selenium_element.rb +12 -0
  14. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  15. data/lib/testa_appium_driver/common/constants.rb +38 -0
  16. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  17. data/lib/testa_appium_driver/common/helpers.rb +272 -0
  18. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +390 -0
  19. data/lib/testa_appium_driver/common/locator.rb +640 -0
  20. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  21. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +371 -0
  22. data/lib/testa_appium_driver/common/scroll_actions.rb +271 -0
  23. data/lib/testa_appium_driver/common/selenium_element.rb +19 -0
  24. data/lib/testa_appium_driver/driver.rb +338 -0
  25. data/lib/testa_appium_driver/ios/driver.rb +49 -0
  26. data/lib/testa_appium_driver/ios/locator/attributes.rb +89 -0
  27. data/lib/testa_appium_driver/ios/locator.rb +73 -0
  28. data/lib/testa_appium_driver/ios/selenium_element.rb +8 -0
  29. data/lib/testa_appium_driver/ios/type_selectors.rb +201 -0
  30. data/lib/testa_appium_driver.rb +4 -0
  31. data/testa_appium_driver.gemspec +6 -2
  32. metadata +39 -11
  33. 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