adb_driver 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/adb_driver.rb +11 -0
- data/lib/adb_driver/adb.rb +123 -0
- data/lib/adb_driver/driver.rb +35 -0
- data/lib/adb_driver/dsl.rb +225 -0
- data/lib/adb_driver/element.rb +72 -0
- data/lib/adb_driver/error.rb +16 -0
- data/lib/adb_driver/finder.rb +61 -0
- data/lib/adb_driver/navigation.rb +7 -0
- data/lib/adb_driver/source_cleaner.rb +60 -0
- data/lib/adb_driver/wait.rb +24 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cc283d720879afb304052b394d127b396c971ff784cb0d6dab39051cc50f88c7
|
|
4
|
+
data.tar.gz: ad04b403c6f63cce562a78150d7db53f7b8eabb8e89d5aeb1d27732377c3e6ba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 790517ac22172520ee9b870cc5a290ae7cb32668b2a5e9be231db9996d9a51f0a5733bbf2245a5439c942b7603977bb0954b38adaf35bfd6f9d38b6a39869581
|
|
7
|
+
data.tar.gz: d446dd2027f102329d4c00ac0f432e476eb965136a6328681830bc714ec92c571e5dca6fc5ae2ac8e5d8f4251cf2abd5c4d4c21c9e136291acde77a6d2c52a73
|
data/lib/adb_driver.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative 'adb_driver/wait'
|
|
2
|
+
|
|
3
|
+
require_relative 'adb_driver/element'
|
|
4
|
+
require_relative 'adb_driver/finder'
|
|
5
|
+
require_relative 'adb_driver/driver'
|
|
6
|
+
require_relative 'adb_driver/navigation'
|
|
7
|
+
require_relative 'adb_driver/error'
|
|
8
|
+
|
|
9
|
+
require_relative 'adb_driver/dsl'
|
|
10
|
+
require_relative 'adb_driver/source_cleaner'
|
|
11
|
+
require_relative 'adb_driver/adb'
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require 'timeout'
|
|
2
|
+
|
|
3
|
+
module Adb
|
|
4
|
+
extend Timeout
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def execute_command(command, timeout_in_seconds = 10)
|
|
8
|
+
timeout(timeout_in_seconds) { `adb #{command}` }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def app_version(package_name)
|
|
12
|
+
output = execute_command("shell dumpsys package #{package_name}")
|
|
13
|
+
version_line = output.lines.grep(/versionName/).first.strip
|
|
14
|
+
version_line[/=(.*)/, 1]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def package_exists?(package_name)
|
|
18
|
+
execute_command('shell pm list packages').lines.grep(/#{package_name}\s*$/).one?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def android_5_or_greater?
|
|
22
|
+
android_version[0].to_i >= 5
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def android_6_or_greater?
|
|
26
|
+
android_version[0].to_i >= 6
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def android_6?
|
|
30
|
+
android_version.start_with?('6')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def android_7?
|
|
34
|
+
android_version.start_with?('7')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def real_device?
|
|
38
|
+
output = execute_command('devices -l')
|
|
39
|
+
output = output.lines.grep(/#{ENV['ANDROID_SERIAL']}/).first
|
|
40
|
+
output.include?('usb')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def emulator?
|
|
44
|
+
!real_device?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def samsung?
|
|
48
|
+
brand == 'samsung'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def htc?
|
|
52
|
+
brand == 'htc'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def asus?
|
|
56
|
+
brand == 'asus'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def lenovo?
|
|
60
|
+
brand == 'Lenovo'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def density
|
|
64
|
+
@density ||= execute_command('shell getprop ro.sf.lcd_density').to_i
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def brand
|
|
68
|
+
execute_command('shell getprop ro.product.brand').strip
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def model
|
|
72
|
+
execute_command('shell getprop ro.product.model').strip
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remove_package(name)
|
|
76
|
+
return unless package_exists?(name)
|
|
77
|
+
`adb uninstall #{name}`
|
|
78
|
+
fail "#{name} package wasn't removed" if package_exists?(name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def restart_adb
|
|
82
|
+
`adb kill-server; adb start-server`
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def portrait?
|
|
86
|
+
@orientation_enum ||= `adb shell dumpsys input`.lines.find { |l| l =~ /SurfaceOrientation/ }.strip[-1].to_i
|
|
87
|
+
@orientation_enum.even?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def connected_device_udid
|
|
91
|
+
case
|
|
92
|
+
when devices.none? then fail('No devices detected')
|
|
93
|
+
when devices.one? then devices.first.udid
|
|
94
|
+
when devices.count > 1 && emulators.none? then fail('Several devices detected. Set ANDROID_SERIAL to pick one')
|
|
95
|
+
when devices.count > 1 && emulators.one? then emulators.first.udid
|
|
96
|
+
when devices.count > 1 && emulators.count > 1 then fail('Several emulators detected. Close all but one')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def android_version
|
|
103
|
+
execute_command('shell getprop ro.build.version.release').lines.last.strip
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def devices
|
|
107
|
+
@device_list ||= begin
|
|
108
|
+
execute_command('devices -l').lines.grep(/model/).inject([]) do |dl, device_string|
|
|
109
|
+
dl << Device.new(device_string[/^\S+/])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def emulators
|
|
115
|
+
@emulators_list ||= begin
|
|
116
|
+
execute_command('devices -l').lines.grep(/model/).grep_v(/usb/).inject([]) do |dl, device_string|
|
|
117
|
+
dl << Device.new(device_string[/^\S+/])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Device = Struct.new(:udid)
|
|
123
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module AdbDriver
|
|
4
|
+
class Driver
|
|
5
|
+
include Finder
|
|
6
|
+
include Wait
|
|
7
|
+
|
|
8
|
+
SCREENSHOT_TIMEOUT = 5
|
|
9
|
+
|
|
10
|
+
attr_reader :logger
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@logger = Logger.new('adb_driver.log')
|
|
14
|
+
@logger.level = Logger::DEBUG
|
|
15
|
+
@logger.info 'Initializing Adb driver'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save_screenshot(filepath)
|
|
19
|
+
if Adb.android_5_or_greater?
|
|
20
|
+
wait(SCREENSHOT_TIMEOUT) { `adb exec-out screencap -p > #{filepath}` }
|
|
21
|
+
else
|
|
22
|
+
wait(SCREENSHOT_TIMEOUT) { `adb shell screencap -p /sdcard/screenshot.png; adb pull /sdcard/screenshot.png #{filepath}` }
|
|
23
|
+
end
|
|
24
|
+
rescue Wait::Error => e
|
|
25
|
+
raise e.class, 'Cannot take a screenshot'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def navigate
|
|
29
|
+
@navigation ||= Navigation.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def quit
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
module DSL
|
|
2
|
+
def active?
|
|
3
|
+
new.active?
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def button(name, options)
|
|
7
|
+
define_active_method(name) if options.delete(:distinctive)
|
|
8
|
+
define_scroll_to_bottom_method(name) if options.delete(:bottom)
|
|
9
|
+
wait_for_class = options.delete(:wait_for_class)
|
|
10
|
+
wait_for_class_timeout = options.delete(:wait_for_class_timeout)
|
|
11
|
+
index = options.delete(:index)
|
|
12
|
+
|
|
13
|
+
locator = options
|
|
14
|
+
define_name_method(name, wait_for_class, wait_for_class_timeout)
|
|
15
|
+
define_query_method(name, locator, index)
|
|
16
|
+
define_button_method(name, locator, index)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def buttons(locator, &block)
|
|
20
|
+
ButtonsBuilder.new(locator, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def radio_buttons(locator, &block)
|
|
24
|
+
ButtonsBuilder.new(locator, &block)
|
|
25
|
+
define_selected_method
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def text_field(name, options)
|
|
29
|
+
define_active_method(name) if options.delete(:distinctive)
|
|
30
|
+
|
|
31
|
+
locator = options
|
|
32
|
+
define_text_field_method(name, locator)
|
|
33
|
+
define_query_method(name, locator)
|
|
34
|
+
define_setter(name, locator)
|
|
35
|
+
define_getter(name, locator)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def switch(name, options)
|
|
39
|
+
define_active_method(name) if options.delete(:distinctive)
|
|
40
|
+
wait_for_class = options.delete(:wait_for_class)
|
|
41
|
+
|
|
42
|
+
locator = options
|
|
43
|
+
define_query_method(name, locator)
|
|
44
|
+
define_switch_methods(name, locator, wait_for_class)
|
|
45
|
+
end
|
|
46
|
+
alias checkbox switch
|
|
47
|
+
|
|
48
|
+
def view(name, options)
|
|
49
|
+
define_active_method(name) if options.delete(:distinctive)
|
|
50
|
+
|
|
51
|
+
locator = options
|
|
52
|
+
define_query_method(name, locator)
|
|
53
|
+
define_view_method(name, locator)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def define_active_method(name)
|
|
59
|
+
define_method(:active?) do
|
|
60
|
+
send("#{name}?")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def define_scroll_to_bottom_method(name)
|
|
65
|
+
define_method(:scroll_to_bottom) do
|
|
66
|
+
start = Time.now
|
|
67
|
+
until send("#{name}?")
|
|
68
|
+
fling_down
|
|
69
|
+
sleep 0.1
|
|
70
|
+
fail "Unable to scroll to the bottom (#{name} button)" if Time.now > start + 60
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def define_name_method(name, wait_for_class, wait_for_class_timeout)
|
|
76
|
+
wait_for_class_timeout = 10 unless wait_for_class_timeout
|
|
77
|
+
|
|
78
|
+
define_method(name) do
|
|
79
|
+
send("#{name}_button").click
|
|
80
|
+
if wait_for_class
|
|
81
|
+
wait(wait_for_class_timeout, "Screen #{wait_for_class} hasn't became active") do
|
|
82
|
+
self.class.const_get(wait_for_class).active?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def define_text_field_method(name, locator)
|
|
89
|
+
define_method("#{name}_text_field") do
|
|
90
|
+
begin
|
|
91
|
+
find_element(locator)
|
|
92
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
|
93
|
+
fail e.class, "'#{name}' text field cannot be found using #{locator}", caller.reject { |line| line =~ /#{__FILE__}/ }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def define_query_method(name, locator, index = nil)
|
|
99
|
+
define_method("#{name}?") do
|
|
100
|
+
if index
|
|
101
|
+
!!find_elements(locator)[index] || false
|
|
102
|
+
else
|
|
103
|
+
begin
|
|
104
|
+
!!find_element(locator)
|
|
105
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def define_button_method(name, locator, index)
|
|
113
|
+
define_method("#{name}_button") do
|
|
114
|
+
if index
|
|
115
|
+
button = find_elements(locator)[index]
|
|
116
|
+
unless button
|
|
117
|
+
fail Selenium::WebDriver::Error::NoSuchElementError,
|
|
118
|
+
"'#{name}' button cannot be found using #{locator} with index #{index}",
|
|
119
|
+
caller.reject { |line| line =~ /#{__FILE__}/ }
|
|
120
|
+
end
|
|
121
|
+
button
|
|
122
|
+
else
|
|
123
|
+
begin
|
|
124
|
+
find_element(locator)
|
|
125
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
|
126
|
+
fail e.class, "'#{name}' button cannot be found using #{locator}", caller.reject { |line| line =~ /#{__FILE__}/ }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def define_view_method(name, locator)
|
|
133
|
+
define_method("#{name}_view") do
|
|
134
|
+
begin
|
|
135
|
+
find_element(locator)
|
|
136
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
|
137
|
+
fail %(Element "#{name}" with locator "#{locator}" wasn't found)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def define_setter(name, locator)
|
|
143
|
+
define_method("#{name}=") do |text|
|
|
144
|
+
find_element(locator).send_keys text
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def define_getter(name, locator, index = nil)
|
|
149
|
+
define_method(name) do
|
|
150
|
+
if index
|
|
151
|
+
find_elements(locator)[index].name
|
|
152
|
+
else
|
|
153
|
+
find_element(locator).name
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def define_switch_methods(name, locator, wait_for_class)
|
|
159
|
+
define_method("#{name}_switch") do
|
|
160
|
+
find_element(locator)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
define_method("toggle_#{name}") do
|
|
164
|
+
find_element(locator).click
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
define_method("#{name}_on?") do
|
|
168
|
+
find_element(locator).attribute(:checked) == 'true'
|
|
169
|
+
end
|
|
170
|
+
alias_method "#{name}_selected?".to_sym, "#{name}_on?".to_sym
|
|
171
|
+
|
|
172
|
+
define_method("turn_on_#{name}") do
|
|
173
|
+
unless send("#{name}_on?")
|
|
174
|
+
send("toggle_#{name}")
|
|
175
|
+
if wait_for_class
|
|
176
|
+
wait(5, "Screen #{wait_for_class} hasn't became active") do
|
|
177
|
+
Module.const_get(wait_for_class).active?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
define_method("turn_off_#{name}") do
|
|
184
|
+
if send("#{name}_on?")
|
|
185
|
+
send("toggle_#{name}")
|
|
186
|
+
if wait_for_class
|
|
187
|
+
wait(5, "Screen #{wait_for_class} hasn't became active") do
|
|
188
|
+
Module.const_get(wait_for_class).active?
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def define_selected_method
|
|
196
|
+
button_methods = instance_methods.grep(/_button/)
|
|
197
|
+
define_method(:selected) do
|
|
198
|
+
result = button_methods.map do |button_method|
|
|
199
|
+
send(button_method).checked? && button_method.to_s.sub(/_button/, '').to_sym
|
|
200
|
+
end
|
|
201
|
+
result.grep_v(false).first
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
class ButtonsBuilder
|
|
206
|
+
def initialize(locator, &block)
|
|
207
|
+
@locator = locator
|
|
208
|
+
@block = block
|
|
209
|
+
@index = 0
|
|
210
|
+
instance_exec(&block)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def method_missing(method, *args)
|
|
214
|
+
button_name = args[0]
|
|
215
|
+
button_params = args[1] || {}
|
|
216
|
+
|
|
217
|
+
unless button_params.key?(:index)
|
|
218
|
+
button_params[:index] = @index
|
|
219
|
+
@index += 1
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@block.binding.receiver.button button_name, @locator.merge(button_params)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
class Element
|
|
2
|
+
Point = Struct.new(:x, :y)
|
|
3
|
+
Size = Struct.new(:width, :height)
|
|
4
|
+
|
|
5
|
+
def initialize(xml_representation)
|
|
6
|
+
@xml_representation = xml_representation
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def click
|
|
10
|
+
`adb shell input tap #{center.x} #{center.y}`
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def long_click
|
|
14
|
+
`adb shell input swipe #{center.x} #{center.y} #{center.x} #{center.y} 800`
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send_keys(text)
|
|
18
|
+
click
|
|
19
|
+
sleep 2
|
|
20
|
+
text.scan(/.{1,10}/).each do |part|
|
|
21
|
+
`adb shell "input text '#{part}'"`
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def text
|
|
26
|
+
@xml_representation.attributes['text']
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def checked?
|
|
30
|
+
@xml_representation.attributes['checked'] == 'true'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def content_desc
|
|
34
|
+
@xml_representation.attributes['content-desc']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def top_left
|
|
38
|
+
Point.new(coordinates[0], coordinates[1])
|
|
39
|
+
end
|
|
40
|
+
alias_method :location, :top_left
|
|
41
|
+
|
|
42
|
+
def lower_right
|
|
43
|
+
Point.new(coordinates[2], coordinates[3])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def width
|
|
47
|
+
lower_right.x - top_left.x
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def height
|
|
51
|
+
lower_right.y - top_left.y
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def center
|
|
55
|
+
Point.new(top_left.x + width / 2, top_left.y + height / 2)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def size
|
|
59
|
+
Size.new(width, height)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def class_name
|
|
63
|
+
@xml_representation.attributes['class']
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def coordinates
|
|
69
|
+
@coordinates ||= @xml_representation.attributes['bounds'].scan(/\d+/).map(&:to_i)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
unless defined?(Selenium)
|
|
2
|
+
module Selenium
|
|
3
|
+
module WebDriver
|
|
4
|
+
module Error
|
|
5
|
+
class NoSuchElementError < StandardError; end
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module AdbDriver
|
|
12
|
+
module Error
|
|
13
|
+
class NoSuchElementError < Selenium::WebDriver::Error::NoSuchElementError; end
|
|
14
|
+
class TimeOutError < StandardError; end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module AdbDriver
|
|
2
|
+
module Finder
|
|
3
|
+
include Wait
|
|
4
|
+
|
|
5
|
+
FIND_ELEMENT_TIMEOUT = 1
|
|
6
|
+
PAGE_SOURCE_TIMEOUT = 15
|
|
7
|
+
MIN_TIME_BETWEEN_FIND_ATTEMPTS = 0.250
|
|
8
|
+
|
|
9
|
+
def find_element(locator)
|
|
10
|
+
wait(FIND_ELEMENT_TIMEOUT) { find_elements(locator).first }
|
|
11
|
+
rescue Wait::Error
|
|
12
|
+
logger.info "Element with locator '#{locator}' has not been found"
|
|
13
|
+
logger.debug "Current hierarchy: #{@last_view_hierarchy}"
|
|
14
|
+
raise Error::NoSuchElementError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find_elements(locator)
|
|
18
|
+
logger.info "Searching for element by: #{locator}"
|
|
19
|
+
|
|
20
|
+
locator_type = locator.first.first
|
|
21
|
+
locator_value = locator.first.last
|
|
22
|
+
|
|
23
|
+
case locator_type
|
|
24
|
+
when :xpath then find_elements_by_xpath(locator_value)
|
|
25
|
+
when :id then find_elements_by_xpath("//*[contains(@resource-id,'#{locator_value}')]")
|
|
26
|
+
when :class_name then find_elements_by_xpath("//*[@class='#{locator_value}']")
|
|
27
|
+
when :content_desc then find_elements_by_xpath("//*[@content-desc='#{locator_value}']")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_elements_by_xpath(xpath)
|
|
32
|
+
sleep 0.05 until Time.now >= (@last_find_attempt_time ||= Time.now) + MIN_TIME_BETWEEN_FIND_ATTEMPTS
|
|
33
|
+
|
|
34
|
+
view_hierarchy = REXML::Document.new(page_source)
|
|
35
|
+
@last_view_hierarchy = view_hierarchy
|
|
36
|
+
|
|
37
|
+
result = view_hierarchy.get_elements(xpath).map { |element| Element.new(element) }
|
|
38
|
+
@last_find_attempt_time = Time.now
|
|
39
|
+
logger.debug "Returning element(s): #{result}"
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def page_source
|
|
44
|
+
adb_command = Adb.android_7? ? 'exec-out uiautomator dump /dev/tty' : 'shell uiautomator dump /dev/tty'
|
|
45
|
+
logger.info "Getting view hierarchy"
|
|
46
|
+
result = Adb.execute_command(adb_command, PAGE_SOURCE_TIMEOUT)
|
|
47
|
+
|
|
48
|
+
if result.empty? || result.strip == 'Killed'
|
|
49
|
+
logger.info "Result is empty or 'Killed'. Restarting adb..."
|
|
50
|
+
Adb.restart_adb
|
|
51
|
+
logger.info "Adb restarted. Getting result..."
|
|
52
|
+
result = Adb.execute_command(adb_command, PAGE_SOURCE_TIMEOUT)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
logger.debug "Result received: #{result}"
|
|
56
|
+
result
|
|
57
|
+
rescue Wait::Error
|
|
58
|
+
raise TimeOutError, "Couldn't get page_source in reasonable time (#{PAGE_SOURCE_TIMEOUT} seconds)"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'rexml/document'
|
|
2
|
+
require 'rexml/sax2listener'
|
|
3
|
+
require 'rexml/parsers/sax2parser'
|
|
4
|
+
|
|
5
|
+
module SourceCleaner
|
|
6
|
+
def self.call(page_source)
|
|
7
|
+
p = REXML::Parsers::SAX2Parser.new(page_source)
|
|
8
|
+
p.listen(MrProper.new)
|
|
9
|
+
p.parse
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class MrProper
|
|
13
|
+
include REXML::SAX2Listener
|
|
14
|
+
|
|
15
|
+
BORING_ATTRS = %w(class index package
|
|
16
|
+
checkable checked clickable
|
|
17
|
+
enabled focusable focused
|
|
18
|
+
scrollable long-clickable password
|
|
19
|
+
selected bounds instance)
|
|
20
|
+
|
|
21
|
+
INDENT_WIDTH = 2
|
|
22
|
+
ATTRIBUTES_INDENT = 4
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@indent = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start_element(uri, localname, qname, attributes)
|
|
29
|
+
return if localname == 'hierarchy'
|
|
30
|
+
|
|
31
|
+
@indent += 1
|
|
32
|
+
|
|
33
|
+
node = ' ' * INDENT_WIDTH * @indent
|
|
34
|
+
node << "class_name: '#{attributes['class']}'"
|
|
35
|
+
|
|
36
|
+
clean_attributes = attributes.delete_if { |k, v| BORING_ATTRS.include?(k) || v.empty? }
|
|
37
|
+
|
|
38
|
+
if value = clean_attributes.delete('resource-id')
|
|
39
|
+
clean_attributes[:id] = value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if value = clean_attributes.delete('content-desc')
|
|
43
|
+
clean_attributes[:content_desc] = value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless clean_attributes.empty?
|
|
47
|
+
node << ' ' * ATTRIBUTES_INDENT
|
|
48
|
+
node << clean_attributes.map { |k, v| "#{k}: '#{v}'" }.join(', ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts node
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def end_element(uri, localname, qname)
|
|
55
|
+
@indent -= 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
SourceCleaner.call(ARGF.read) if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Wait
|
|
2
|
+
Error = Class.new(StandardError)
|
|
3
|
+
|
|
4
|
+
def self.included(mod)
|
|
5
|
+
mod.extend self
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def wait(duration = 10, message = nil, &block)
|
|
9
|
+
polling_interaval = 0.1 # 100 msec
|
|
10
|
+
start = Time.now
|
|
11
|
+
|
|
12
|
+
loop do
|
|
13
|
+
sleep polling_interaval
|
|
14
|
+
|
|
15
|
+
result = block.call
|
|
16
|
+
break result if result
|
|
17
|
+
|
|
18
|
+
next if Time.now < start + duration
|
|
19
|
+
|
|
20
|
+
cleaned_stacktrace = caller.reverse.take_while { |line| line !~ /#{__FILE__}.*#{__method__}/ }.reverse
|
|
21
|
+
raise Error, (message || 'Wait::Error'), cleaned_stacktrace
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: adb_driver
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Anton Petrov
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-05-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A driver with Seleium-like interface to interact with Android devices
|
|
14
|
+
directly via ADB
|
|
15
|
+
email: givemeletter@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/adb_driver.rb
|
|
21
|
+
- lib/adb_driver/adb.rb
|
|
22
|
+
- lib/adb_driver/driver.rb
|
|
23
|
+
- lib/adb_driver/dsl.rb
|
|
24
|
+
- lib/adb_driver/element.rb
|
|
25
|
+
- lib/adb_driver/error.rb
|
|
26
|
+
- lib/adb_driver/finder.rb
|
|
27
|
+
- lib/adb_driver/navigation.rb
|
|
28
|
+
- lib/adb_driver/source_cleaner.rb
|
|
29
|
+
- lib/adb_driver/wait.rb
|
|
30
|
+
homepage: https://github.com/sofaking/adb_driver
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata: {}
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubyforge_project:
|
|
50
|
+
rubygems_version: 2.7.4
|
|
51
|
+
signing_key:
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: A driver with Seleium-like interface to interact with Android devices directly
|
|
54
|
+
via ADB
|
|
55
|
+
test_files: []
|