testa_appium_driver 0.1.0

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.idea/deployment.xml +21 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +9 -0
  5. data/.idea/misc.xml +6 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/runConfigurations/Android_Test.xml +42 -0
  8. data/.idea/sshConfigs.xml +10 -0
  9. data/.idea/vcs.xml +6 -0
  10. data/.idea/webServers.xml +14 -0
  11. data/.rspec +3 -0
  12. data/.rubocop.yml +13 -0
  13. data/CHANGELOG.md +5 -0
  14. data/CODE_OF_CONDUCT.md +102 -0
  15. data/Gemfile +12 -0
  16. data/LICENSE.txt +21 -0
  17. data/README.md +52 -0
  18. data/Rakefile +12 -0
  19. data/bin/console +17 -0
  20. data/bin/setup +8 -0
  21. data/lib/testa_appium_driver.rb +6 -0
  22. data/lib/testa_appium_driver/android/class_selectors.rb +353 -0
  23. data/lib/testa_appium_driver/android/driver.rb +52 -0
  24. data/lib/testa_appium_driver/android/locator.rb +115 -0
  25. data/lib/testa_appium_driver/android/locator/attributes.rb +116 -0
  26. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +77 -0
  27. data/lib/testa_appium_driver/android/selenium_element.rb +8 -0
  28. data/lib/testa_appium_driver/common/bounds.rb +150 -0
  29. data/lib/testa_appium_driver/common/constants.rb +33 -0
  30. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +12 -0
  31. data/lib/testa_appium_driver/common/helpers.rb +242 -0
  32. data/lib/testa_appium_driver/common/locator.rb +371 -0
  33. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +287 -0
  34. data/lib/testa_appium_driver/common/scroll_actions.rb +253 -0
  35. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +4 -0
  36. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +261 -0
  37. data/lib/testa_appium_driver/driver.rb +226 -0
  38. data/lib/testa_appium_driver/ios/driver.rb +35 -0
  39. data/lib/testa_appium_driver/ios/locator.rb +40 -0
  40. data/lib/testa_appium_driver/ios/locator/attributes.rb +79 -0
  41. data/lib/testa_appium_driver/ios/selenium_element.rb +7 -0
  42. data/lib/testa_appium_driver/ios/type_selectors.rb +167 -0
  43. data/lib/testa_appium_driver/version.rb +5 -0
  44. data/testa_appium_driver.gemspec +40 -0
  45. data/testa_appium_driver.iml +79 -0
  46. metadata +147 -0
@@ -0,0 +1,52 @@
1
+ require_relative 'class_selectors'
2
+ require_relative 'locator'
3
+ require_relative 'scroll_actions/uiautomator_scroll_actions'
4
+ require_relative 'selenium_element'
5
+
6
+ module TestaAppiumDriver
7
+ class Driver
8
+ include ClassSelectors
9
+
10
+
11
+ # @param [String] command Shell command name to execute for example echo or rm
12
+ # @param [Array<String>] args Array of command arguments, example: ['-f', '/sdcard/my_file.txt']
13
+ # @param [Integer] timeout Command timeout in milliseconds. If the command blocks for longer than this timeout then an exception is going to be thrown. The default timeout is 20000 ms
14
+ # @param [Boolean] includeStderr Whether to include stderr stream into the returned result.
15
+ #noinspection RubyParameterNamingConvention
16
+ def shell(command, args: nil, timeout: nil, includeStderr: true)
17
+ params = {
18
+ command: command,
19
+ includeStderr: includeStderr
20
+ }
21
+ params[:args] = args unless args.nil?
22
+ params[:timeout] = timeout unless timeout.nil?
23
+ @driver.execute_script("mobile: shell", params)
24
+ end
25
+
26
+
27
+ def handle_testa_opts
28
+ if @testa_opts[:default_find_strategy].nil?
29
+ @default_find_strategy = DEFAULT_ANDROID_FIND_STRATEGY
30
+ else
31
+ case @testa_opts[:default_find_strategy].to_sym
32
+ when FIND_STRATEGY_UIAUTOMATOR, FIND_STRATEGY_XPATH
33
+ @default_find_strategy = @testa_opts[:default_find_strategy].to_sym
34
+ else
35
+ raise "Default find strategy #{@testa_opts[:default_find_strategy]} not supported for Android"
36
+ end
37
+ end
38
+
39
+
40
+ if @testa_opts[:default_scroll_strategy].nil?
41
+ @default_scroll_strategy = DEFAULT_ANDROID_SCROLL_STRATEGY
42
+ else
43
+ case @testa_opts[:default_scroll_strategy].to_sym
44
+ when SCROLL_STRATEGY_W3C, SCROLL_STRATEGY_UIAUTOMATOR
45
+ @default_scroll_strategy = @testa_opts[:default_scroll_strategy].to_sym
46
+ else
47
+ raise "Default scroll strategy #{@testa_opts[:default_scroll_strategy]} not supported for Android"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,115 @@
1
+ require_relative 'locator/attributes'
2
+
3
+ module TestaAppiumDriver
4
+ #noinspection RubyTooManyInstanceVariablesInspection
5
+ class Locator
6
+ attr_accessor :closing_parenthesis
7
+ include ClassSelectors
8
+
9
+ def init(params, selectors, single)
10
+ @closing_parenthesis = 0
11
+
12
+
13
+ @ui_selector = hash_to_uiautomator(selectors, single)
14
+
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
+ params[:scrollable_locator] = self.dup
23
+ end
24
+
25
+ @scrollable_locator = params[:scrollable_locator] if params[:scrollable_locator]
26
+ end
27
+
28
+
29
+ # resolve selector which will be used for finding element
30
+ def selector
31
+ if (@strategy.nil? && @default_find_strategy == FIND_STRATEGY_UIAUTOMATOR) || @strategy == FIND_STRATEGY_UIAUTOMATOR
32
+ ui_selector
33
+ elsif (@strategy.nil? && @default_find_strategy == FIND_STRATEGY_XPATH) || @strategy == FIND_STRATEGY_XPATH
34
+ @xpath_selector
35
+ end
36
+ end
37
+
38
+
39
+
40
+ # @param [Boolean] include_semicolon should the semicolon be included at the end
41
+ # @return ui_selector for uiautomator find strategy
42
+ def ui_selector(include_semicolon = true)
43
+ @ui_selector + ")" * @closing_parenthesis + (include_semicolon ? ";" : "");
44
+ end
45
+
46
+ def ui_selector=(value)
47
+ @ui_selector = value
48
+ end
49
+
50
+
51
+
52
+
53
+ # @return [TestaAppiumDriver::Locator]
54
+ def from_parent(selectors = {})
55
+ raise "Cannot add from_parent selector to array" unless @single
56
+ raise StrategyMixException.new(@strategy, @strategy_reason, FIND_STRATEGY_UIAUTOMATOR, "from_parent") if @strategy != FIND_STRATEGY_UIAUTOMATOR
57
+
58
+ locator = self.dup
59
+ locator.strategy = FIND_STRATEGY_UIAUTOMATOR
60
+ locator.strategy_reason = "from_parent"
61
+ locator.closing_parenthesis += 1
62
+ locator.ui_selector = "#{locator.ui_selector}.fromParent(#{hash_to_uiautomator(selectors)}"
63
+ locator
64
+ end
65
+
66
+
67
+ # @return [Locator] existing locator element
68
+ def add_child_selector(params)
69
+ params, selectors = extract_selectors_from_params(params)
70
+ single = params[:single]
71
+ raise "Cannot add child selector to Array" if single && !@single
72
+
73
+ locator = self.dup
74
+ if (@strategy.nil? && !single) || @strategy == FIND_STRATEGY_XPATH
75
+ locator.strategy = FIND_STRATEGY_XPATH
76
+ locator.strategy_reason = "multiple child selector"
77
+ add_xpath_child_selectors(locator, selectors, single)
78
+ elsif @strategy == FIND_STRATEGY_UIAUTOMATOR
79
+ locator = add_uiautomator_child_selector(locator, selectors, single)
80
+ else
81
+ # both paths are valid
82
+ add_xpath_child_selectors(locator, selectors, single)
83
+ locator = add_uiautomator_child_selector(locator, selectors, single)
84
+ end
85
+
86
+ if is_scrollable_selector?(selectors, single)
87
+ locator.scrollable_locator = self
88
+ if selectors[:class] == "android.widget.HorizontalScrollView"
89
+ locator.scrollable_locator.scroll_orientation = :horizontal
90
+ else
91
+ locator.scrollable_locator.scroll_orientation = :vertical
92
+ end
93
+ end
94
+
95
+ locator.last_selector_adjacent = false
96
+ locator
97
+ end
98
+
99
+
100
+ private
101
+ def add_uiautomator_child_selector(locator, selectors, single)
102
+ if locator.single && !single
103
+ # current locator stays single, the child locator looks for multiple
104
+ params = selectors.merge({single: single, scrollable_locator: locator.scrollable_locator})
105
+ params[:default_find_strategy] = locator.default_find_strategy
106
+ params[:default_scroll_strategy] = locator.default_scroll_strategy
107
+ Locator.new(@driver, self, params)
108
+ else
109
+ locator.single = true
110
+ locator.ui_selector = "#{locator.ui_selector(false)}.childSelector(#{hash_to_uiautomator(selectors, single)})"
111
+ locator
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,116 @@
1
+ module TestaAppiumDriver
2
+ module AndroidAttributeModule
3
+
4
+ #noinspection RubyNilAnalysis
5
+ def attribute(name, *args)
6
+ elements = execute(*args)
7
+
8
+ @driver = get_driver if self.instance_of?(Selenium::WebDriver::Element)
9
+
10
+ @driver.disable_wait_for_idle
11
+ if elements.kind_of?(Selenium::WebDriver::Element)
12
+ r = elements.send(:attribute, name.to_s)
13
+ r = TestaAppiumDriver::Bounds.from_android(r, @driver) if name.to_s == "bounds"
14
+ else
15
+ r = elements.map { |e| e.send(:attribute, name.to_s) }
16
+ r.map! { |b| TestaAppiumDriver::Bounds.from_android(b, @driver) } if name.to_s == "bounds"
17
+ end
18
+ @driver.enable_wait_for_idle
19
+ r
20
+ end
21
+
22
+ def text(*args)
23
+ attribute("text", *args)
24
+ end
25
+
26
+ def package(*args)
27
+ attribute("package", *args)
28
+ end
29
+
30
+ def class_name(*args)
31
+ attribute("className", *args)
32
+ end
33
+
34
+ def checkable?(*args)
35
+ attribute("checkable", *args).to_s == "true"
36
+ end
37
+
38
+ def checked?(*args)
39
+ attribute("checked", *args).to_s == "true"
40
+ end
41
+
42
+ def clickable?(*args)
43
+ attribute("clickable", *args).to_s == "true"
44
+ end
45
+
46
+ def desc(*args)
47
+ attribute("contentDescription", *args)
48
+ end
49
+
50
+ def enabled?(*args)
51
+ attribute("enabled", *args).to_s == "true"
52
+ end
53
+
54
+ def focusable?(*args)
55
+ attribute("focusable", *args).to_s == "true"
56
+ end
57
+
58
+ def focused?(*args)
59
+ attribute("focused", *args).to_s == "true"
60
+ end
61
+
62
+ def long_clickable?(*args)
63
+ attribute("longClickable", *args).to_s == "true"
64
+ end
65
+
66
+ def password(*args)
67
+ attribute("password", *args)
68
+ end
69
+
70
+ def id(*args)
71
+ attribute("resourceId", *args)
72
+ end
73
+
74
+ def scrollable?(*args)
75
+ attribute("scrollable", *args).to_s == "true"
76
+ end
77
+
78
+ def selected?(*args)
79
+ attribute("selected", *args).to_s == "true"
80
+ end
81
+
82
+ def displayed?(*args)
83
+ attribute("displayed", *args).to_s == "true"
84
+ end
85
+
86
+ def selection_start(*args)
87
+ attribute("selection-start", *args)
88
+ end
89
+
90
+ def selection_end(*args)
91
+ attribute("selection-end", *args)
92
+ end
93
+
94
+ def bounds(*args)
95
+ attribute("bounds", *args)
96
+ end
97
+
98
+ end
99
+
100
+ class Locator
101
+ include TestaAppiumDriver::AndroidAttributeModule
102
+
103
+
104
+ # element index in parent element, starts from 0
105
+ #noinspection RubyNilAnalysis,RubyYardReturnMatch
106
+ # @return [Integer, nil] index of element
107
+ def index(*args)
108
+ raise "Index not supported for uiautomator strategy" if @strategy == FIND_STRATEGY_UIAUTOMATOR
109
+ this = execute(*args)
110
+ children = self.dup.parent.children.execute
111
+ index = children.index(this)
112
+ raise "Index not found" if index.nil?
113
+ index
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,77 @@
1
+ module TestaAppiumDriver
2
+ #noinspection RubyInstanceMethodNamingConvention
3
+ class ScrollActions
4
+ private
5
+
6
+ def uiautomator_scroll_to_start_or_end(type)
7
+ @driver.disable_wait_for_idle
8
+ @driver.disable_implicit_wait
9
+
10
+ scrollable_selector = @scrollable.ui_selector(false)
11
+ orientation = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
12
+ scroll_command = type == :start ? ".scrollToBeginning(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})" : ".scrollToEnd(#{DEFAULT_UIAUTOMATOR_MAX_SWIPES})"
13
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{scroll_command};"
14
+ begin
15
+ puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
16
+ @driver.find_element(uiautomator: cmd)
17
+ rescue
18
+ # Ignored
19
+ end
20
+
21
+
22
+ @driver.enable_implicit_wait
23
+ @driver.enable_wait_for_idle
24
+ end
25
+
26
+
27
+ def uiautomator_scroll_to
28
+ raise "UiAutomator scroll cannot work with specified direction" unless @direction.nil?
29
+
30
+
31
+ @driver.disable_wait_for_idle
32
+ @driver.disable_implicit_wait
33
+
34
+ scrollable_selector = @scrollable.ui_selector(false)
35
+ element_selector = @locator.ui_selector(false)
36
+ orientation_command = @scrollable.scroll_orientation == :vertical ? ".setAsVerticalList()" : ".setAsHorizontalList()"
37
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation_command}.scrollIntoView(#{element_selector});"
38
+ begin
39
+ puts "Scroll execute[uiautomator_scroll_to]: #{cmd}"
40
+ @driver.find_element(uiautomator: cmd)
41
+ rescue
42
+ # Ignored
43
+ ensure
44
+ @driver.enable_implicit_wait
45
+ @driver.enable_wait_for_idle
46
+ end
47
+ end
48
+
49
+
50
+ def uiautomator_page_or_fling(type, direction)
51
+ @driver.disable_wait_for_idle
52
+ @driver.disable_implicit_wait
53
+
54
+ scrollable_selector = @scrollable.ui_selector(false)
55
+ orientation = direction == :up || direction == :down ? ".setAsVerticalList()" : ".setAsHorizontalList()"
56
+ if type == SCROLL_ACTION_TYPE_SCROLL
57
+ direction_command = direction == :down || direction == :right ? ".scrollForward()" : ".scrollBackward()"
58
+ elsif type == SCROLL_ACTION_TYPE_FLING
59
+ direction_command = direction == :down || direction == :right ? ".flingForward()" : ".flingBackward()"
60
+ else
61
+ raise "Unknown scroll action type #{type}"
62
+ end
63
+ cmd = "new UiScrollable(#{scrollable_selector})#{orientation}#{direction_command};"
64
+ begin
65
+ puts "Scroll execute[uiautomator_#{type}]: #{cmd}"
66
+ @driver.find_element(uiautomator: cmd)
67
+ rescue
68
+ # Ignored
69
+ end
70
+ @driver.enable_implicit_wait
71
+ @driver.enable_wait_for_idle
72
+ end
73
+
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,8 @@
1
+ module Selenium
2
+ module WebDriver
3
+ class Element
4
+ include TestaAppiumDriver::ClassSelectors
5
+ include TestaAppiumDriver::AndroidAttributeModule
6
+ end
7
+ end
8
+ 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