adb_driver 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|