calabash-android 0.5.1 → 0.5.2.pre1
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 +4 -4
- data/Gemfile.lock +1 -1
- data/bin/calabash-android-console.rb +24 -9
- data/bin/calabash-android-run.rb +4 -1
- data/bin/calabash-android-setup.rb +1 -1
- data/lib/calabash-android/deprecated_actions.map +8 -0
- data/lib/calabash-android/env.rb +30 -2
- data/lib/calabash-android/environment_helpers.rb +12 -0
- data/lib/calabash-android/gestures.rb +308 -0
- data/lib/calabash-android/helpers.rb +55 -6
- data/lib/calabash-android/java_keystore.rb +2 -1
- data/lib/calabash-android/lib/TestServer.apk +0 -0
- data/lib/calabash-android/operations.rb +901 -707
- data/lib/calabash-android/removed_actions.txt +4 -0
- data/lib/calabash-android/steps/date_picker_steps.rb +4 -4
- data/lib/calabash-android/steps/time_picker_steps.rb +3 -3
- data/lib/calabash-android/touch_helpers.rb +114 -18
- data/lib/calabash-android/version.rb +1 -1
- data/test-server/calabash-js/src/calabash.js +42 -1
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/HttpServer.java +67 -2
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/MultiTouchGesture.java +749 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/softkey/PressKey.java +85 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/HideSoftKeyboard.java +24 -2
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/PressUserActionButton.java +128 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/webview/QueryHelper.java +8 -1
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/query/InvocationOperation.java +56 -9
- metadata +10 -8
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByContentDescription.java +0 -33
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByIndex.java +0 -24
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByContentDescription.java +0 -34
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByIndex.java +0 -26
@@ -14,11 +14,58 @@ def package_name(app)
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def main_activity(app)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
begin
|
18
|
+
log("Trying to find launchable activity")
|
19
|
+
launchable_activity_line = aapt_dump(app, "launchable-activity").first
|
20
|
+
raise "'launchable-activity' not found in aapt output" unless launchable_activity_line
|
21
|
+
m = launchable_activity_line.match(/name='([^']+)'/)
|
22
|
+
raise "Unexpected output from aapt: #{launchable_activity_line}" unless m
|
23
|
+
log("Found launchable activity '#{m[1]}'")
|
24
|
+
m[1]
|
25
|
+
rescue => e
|
26
|
+
log("Could not find launchable activity, trying to parse raw AndroidManifest. #{e.message}")
|
27
|
+
|
28
|
+
manifest_data = `"#{Env.tools_dir}/aapt" dump xmltree "#{app}" AndroidManifest.xml`
|
29
|
+
regex = /^\s*A:[\s*]android:name\(\w+\)\=\"android.intent.category.LAUNCHER\"/
|
30
|
+
lines = manifest_data.lines.collect(&:strip)
|
31
|
+
indicator_line = nil
|
32
|
+
|
33
|
+
lines.each_with_index do |line, index|
|
34
|
+
match = line.match(regex)
|
35
|
+
|
36
|
+
unless match.nil?
|
37
|
+
raise 'More than one launchable activity in AndroidManifest' unless indicator_line.nil?
|
38
|
+
indicator_line = index
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
raise 'No launchable activity found in AndroidManifest' unless indicator_line
|
43
|
+
|
44
|
+
intent_filter_found = false
|
45
|
+
|
46
|
+
(0..indicator_line).reverse_each do |index|
|
47
|
+
if intent_filter_found
|
48
|
+
match = lines[index].match(/\s*E:\s*activity-alias/)
|
49
|
+
|
50
|
+
raise 'Could not find target activity in activity alias' if match
|
51
|
+
|
52
|
+
match = lines[index].match(/^\s*A:\s*android:targetActivity\(\w*\)\=\"([^\"]+)/){$1}
|
53
|
+
|
54
|
+
if match
|
55
|
+
log("Found launchable activity '#{match}'")
|
56
|
+
|
57
|
+
return match
|
58
|
+
end
|
59
|
+
else
|
60
|
+
unless lines[index].match(/\s*E: intent-filter/).nil?
|
61
|
+
log("Read intent filter")
|
62
|
+
intent_filter_found = true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
raise 'Could not find launchable activity'
|
68
|
+
end
|
22
69
|
end
|
23
70
|
|
24
71
|
def aapt_dump(app, key)
|
@@ -69,7 +116,9 @@ def unsign_apk(path)
|
|
69
116
|
end
|
70
117
|
|
71
118
|
def zipalign_apk(inpath, outpath)
|
72
|
-
|
119
|
+
cmd = %Q("#{Env.zipalign_path}" -f 4 "#{inpath}" "#{outpath}")
|
120
|
+
log "Zipaligning using: #{cmd}"
|
121
|
+
system(cmd)
|
73
122
|
end
|
74
123
|
|
75
124
|
def sign_apk(app_path, dest_path)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class JavaKeystore
|
2
2
|
attr_reader :errors, :location, :keystore_alias, :password, :fingerprint
|
3
3
|
def initialize(location, keystore_alias, password)
|
4
|
-
raise "No such file #{location}" unless File.exists?(File.expand_path(location))
|
4
|
+
raise "No such keystore file '#{location}'" unless File.exists?(File.expand_path(location))
|
5
5
|
|
6
6
|
keystore_data = system_with_stdout_on_success(Env.keytool_path, '-list', '-v', '-alias', keystore_alias, '-keystore', location, '-storepass', password, '-J"-Dfile.encoding=utf-8"')
|
7
7
|
if keystore_data.nil?
|
@@ -79,6 +79,7 @@ class JavaKeystore
|
|
79
79
|
fail_if_key_missing(keystore, "keystore_password")
|
80
80
|
fail_if_key_missing(keystore, "keystore_alias")
|
81
81
|
keystore["keystore_location"] = File.expand_path(keystore["keystore_location"])
|
82
|
+
log("Keystore location specified in #{File.exist?(".calabash_settings") ? ".calabash_settings" : "calabash_settings"}.")
|
82
83
|
JavaKeystore.new(keystore["keystore_location"], keystore["keystore_alias"], keystore["keystore_password"])
|
83
84
|
end
|
84
85
|
|
Binary file
|
@@ -6,7 +6,9 @@ require 'rubygems'
|
|
6
6
|
require 'json'
|
7
7
|
require 'socket'
|
8
8
|
require 'timeout'
|
9
|
+
require 'calabash-android/gestures'
|
9
10
|
require 'calabash-android/helpers'
|
11
|
+
require 'calabash-android/environment_helpers'
|
10
12
|
require 'calabash-android/text_helpers'
|
11
13
|
require 'calabash-android/touch_helpers'
|
12
14
|
require 'calabash-android/wait_helpers'
|
@@ -14,526 +16,601 @@ require 'calabash-android/version'
|
|
14
16
|
require 'calabash-android/env'
|
15
17
|
require 'retriable'
|
16
18
|
require 'cucumber'
|
19
|
+
require 'date'
|
20
|
+
require 'time'
|
17
21
|
|
18
22
|
|
19
23
|
module Calabash module Android
|
20
24
|
|
21
|
-
module Operations
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
module Operations
|
26
|
+
include Calabash::Android::EnvironmentHelpers
|
27
|
+
include Calabash::Android::TextHelpers
|
28
|
+
include Calabash::Android::TouchHelpers
|
29
|
+
include Calabash::Android::WaitHelpers
|
25
30
|
|
26
|
-
|
27
|
-
|
28
|
-
|
31
|
+
def current_activity
|
32
|
+
`#{default_device.adb_command} shell dumpsys window windows`.each_line.grep(/mFocusedApp.+[\.\/]([^.\s\/\}]+)/){$1}.first
|
33
|
+
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
35
|
+
def log(message)
|
36
|
+
$stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{message}" if (ARGV.include? "-v" or ARGV.include? "--verbose")
|
37
|
+
end
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
def macro(txt)
|
40
|
+
if self.respond_to?(:step)
|
41
|
+
step(txt)
|
42
|
+
else
|
43
|
+
Then(txt)
|
44
|
+
end
|
39
45
|
end
|
40
|
-
end
|
41
46
|
|
42
|
-
|
43
|
-
|
44
|
-
|
47
|
+
def default_device
|
48
|
+
unless @default_device
|
49
|
+
@default_device = Device.new(self, ENV["ADB_DEVICE_ARG"], ENV["TEST_SERVER_PORT"], ENV["APP_PATH"], ENV["TEST_APP_PATH"])
|
50
|
+
end
|
51
|
+
@default_device
|
45
52
|
end
|
46
|
-
@default_device
|
47
|
-
end
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
|
54
|
+
def set_default_device(device)
|
55
|
+
@default_device = device
|
56
|
+
end
|
52
57
|
|
53
|
-
|
54
|
-
|
58
|
+
def performAction(action, *arguments)
|
59
|
+
puts "Warning: The method performAction is deprecated. Please use perform_action instead."
|
55
60
|
|
56
|
-
|
57
|
-
|
61
|
+
perform_action(action, *arguments)
|
62
|
+
end
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
def perform_action(action, *arguments)
|
65
|
+
if removed_actions.include?(action)
|
66
|
+
puts "\e[31mError: The action '#{action}' was removed in calabash-android 0.5\e[0m"
|
67
|
+
puts 'Solutions that do not require the removed action can be found on:'
|
68
|
+
puts "\e[36mhttps://github.com/calabash/calabash-android/blob/master/migrating_to_calabash_0.5.md\##{action}\e[0m"
|
69
|
+
elsif deprecated_actions.has_key?(action)
|
70
|
+
puts "\e[31mWarning: The action '#{action}' is deprecated\e[0m"
|
71
|
+
puts "\e[32mUse '#{deprecated_actions[action]}' instead\e[0m"
|
72
|
+
end
|
62
73
|
|
63
|
-
|
64
|
-
puts "\e[31mError: The action '#{action}' was removed in calabash-android 0.5\e[0m"
|
65
|
-
puts 'Solutions that do not require the removed action can be found on:'
|
66
|
-
puts "\e[36mhttps://github.com/calabash/calabash-android/blob/master/migrating_to_calabash_0.5.md\##{action}\e[0m"
|
74
|
+
default_device.perform_action(action, *arguments)
|
67
75
|
end
|
68
|
-
|
69
|
-
default_device.perform_action(action, *arguments)
|
70
|
-
end
|
71
76
|
|
72
|
-
|
73
|
-
|
74
|
-
|
77
|
+
def removed_actions
|
78
|
+
@removed_actions ||= File.readlines(File.join(File.dirname(__FILE__), 'removed_actions.txt')).map(&:chomp)
|
79
|
+
end
|
75
80
|
|
76
|
-
|
77
|
-
|
78
|
-
|
81
|
+
def deprecated_actions
|
82
|
+
@deprecated_actions ||= Hash[
|
83
|
+
*File.readlines(File.join(File.dirname(__FILE__), 'deprecated_actions.map')).map{|e| e.chomp.split(',', 2)}.flatten
|
84
|
+
]
|
85
|
+
end
|
79
86
|
|
80
|
-
|
81
|
-
|
82
|
-
|
87
|
+
def reinstall_apps
|
88
|
+
default_device.reinstall_apps
|
89
|
+
end
|
83
90
|
|
84
|
-
|
85
|
-
|
86
|
-
|
91
|
+
def reinstall_test_server
|
92
|
+
default_device.reinstall_test_server
|
93
|
+
end
|
87
94
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
95
|
+
def install_app(app_path)
|
96
|
+
default_device.install_app(app_path)
|
97
|
+
end
|
92
98
|
|
93
|
-
|
94
|
-
|
95
|
-
|
99
|
+
def update_app(app_path)
|
100
|
+
default_device.update_app(app_path)
|
101
|
+
end
|
96
102
|
|
97
|
-
|
98
|
-
|
99
|
-
|
103
|
+
def uninstall_apps
|
104
|
+
default_device.uninstall_app(package_name(default_device.test_server_path))
|
105
|
+
default_device.uninstall_app(package_name(default_device.app_path))
|
106
|
+
end
|
100
107
|
|
101
|
-
|
102
|
-
|
103
|
-
|
108
|
+
def wake_up
|
109
|
+
default_device.wake_up()
|
110
|
+
end
|
104
111
|
|
105
|
-
|
106
|
-
|
107
|
-
|
112
|
+
def clear_app_data
|
113
|
+
default_device.clear_app_data
|
114
|
+
end
|
108
115
|
|
109
|
-
|
110
|
-
|
111
|
-
|
116
|
+
def pull(remote, local)
|
117
|
+
default_device.pull(remote, local)
|
118
|
+
end
|
112
119
|
|
113
|
-
|
114
|
-
|
115
|
-
|
120
|
+
def push(local, remote)
|
121
|
+
default_device.push(local, remote)
|
122
|
+
end
|
116
123
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
end
|
124
|
+
def start_test_server_in_background(options={})
|
125
|
+
default_device.start_test_server_in_background(options)
|
126
|
+
end
|
121
127
|
|
122
|
-
|
123
|
-
|
124
|
-
|
128
|
+
def shutdown_test_server
|
129
|
+
default_device.shutdown_test_server
|
130
|
+
end
|
125
131
|
|
126
|
-
|
127
|
-
|
128
|
-
|
132
|
+
def screenshot_embed(options={:prefix => nil, :name => nil, :label => nil})
|
133
|
+
path = default_device.screenshot(options)
|
134
|
+
embed(path, "image/png", options[:label] || File.basename(path))
|
135
|
+
end
|
129
136
|
|
130
|
-
|
131
|
-
|
132
|
-
|
137
|
+
def screenshot(options={:prefix => nil, :name => nil})
|
138
|
+
default_device.screenshot(options)
|
139
|
+
end
|
133
140
|
|
134
|
-
|
135
|
-
|
136
|
-
|
141
|
+
def client_version
|
142
|
+
default_device.client_version
|
143
|
+
end
|
137
144
|
|
138
|
-
|
139
|
-
|
140
|
-
|
145
|
+
def server_version
|
146
|
+
default_device.server_version
|
147
|
+
end
|
141
148
|
|
142
|
-
|
143
|
-
|
144
|
-
|
149
|
+
def fail(msg="Error. Check log for details.", options={:prefix => nil, :name => nil, :label => nil})
|
150
|
+
screenshot_and_raise(msg, options)
|
151
|
+
end
|
145
152
|
|
146
|
-
|
147
|
-
|
148
|
-
|
153
|
+
def set_gps_coordinates_from_location(location)
|
154
|
+
default_device.set_gps_coordinates_from_location(location)
|
155
|
+
end
|
149
156
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
if arg.is_a?(Hash) and arg.count == 1
|
154
|
-
if arg.values.is_a?(Array) && arg.values.count == 1
|
155
|
-
values = arg.values.flatten
|
156
|
-
else
|
157
|
-
values = [arg.values]
|
158
|
-
end
|
157
|
+
def set_gps_coordinates(latitude, longitude)
|
158
|
+
default_device.set_gps_coordinates(latitude, longitude)
|
159
|
+
end
|
159
160
|
|
160
|
-
|
161
|
-
|
162
|
-
converted_args << arg
|
163
|
-
end
|
161
|
+
def get_preferences(name)
|
162
|
+
default_device.get_preferences(name)
|
164
163
|
end
|
165
|
-
map(uiquery,:query,*converted_args)
|
166
|
-
end
|
167
164
|
|
168
|
-
|
169
|
-
|
170
|
-
|
165
|
+
def set_preferences(name, hash)
|
166
|
+
default_device.set_preferences(name, hash)
|
167
|
+
end
|
171
168
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
stop_when = opts[:stop_when] || lambda { |i| false }
|
176
|
-
check_element_exists(uiquery)
|
177
|
-
num_items = query(opts[:query], :adapter, :count).first
|
178
|
-
num_items.times do |item|
|
179
|
-
next if skip_if.call(item)
|
180
|
-
break if stop_when.call(item)
|
169
|
+
def clear_preferences(name)
|
170
|
+
default_device.clear_preferences(name)
|
171
|
+
end
|
181
172
|
|
182
|
-
|
183
|
-
|
184
|
-
|
173
|
+
def query(uiquery, *args)
|
174
|
+
converted_args = []
|
175
|
+
args.each do |arg|
|
176
|
+
if arg.is_a?(Hash) and arg.count == 1
|
177
|
+
if arg.values.is_a?(Array) && arg.values.count == 1
|
178
|
+
values = arg.values.flatten
|
179
|
+
else
|
180
|
+
values = [arg.values]
|
181
|
+
end
|
182
|
+
|
183
|
+
converted_args << {:method_name => arg.keys.first, :arguments => values}
|
184
|
+
else
|
185
|
+
converted_args << arg
|
186
|
+
end
|
187
|
+
end
|
188
|
+
map(uiquery,:query,*converted_args)
|
185
189
|
end
|
186
|
-
end
|
187
190
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
+
def flash(query_string)
|
192
|
+
map(query_string, :flash)
|
193
|
+
end
|
191
194
|
|
192
|
-
|
195
|
+
def each_item(opts={:query => "android.widget.ListView", :post_scroll => 0.2}, &block)
|
196
|
+
uiquery = opts[:query] || "android.widget.ListView"
|
197
|
+
skip_if = opts[:skip_if] || lambda { |i| false }
|
198
|
+
stop_when = opts[:stop_when] || lambda { |i| false }
|
199
|
+
check_element_exists(uiquery)
|
200
|
+
num_items = query(opts[:query], :adapter, :count).first
|
201
|
+
num_items.times do |item|
|
202
|
+
next if skip_if.call(item)
|
203
|
+
break if stop_when.call(item)
|
204
|
+
|
205
|
+
scroll_to_row(opts[:query], item)
|
206
|
+
sleep(opts[:post_scroll]) if opts[:post_scroll] and opts[:post_scroll] > 0
|
207
|
+
yield(item)
|
208
|
+
end
|
209
|
+
end
|
193
210
|
|
194
|
-
|
211
|
+
def set_date(query_string, year_or_datestring, month=nil, day=nil)
|
212
|
+
wait_for_element_exists(query_string)
|
195
213
|
|
196
|
-
|
197
|
-
|
198
|
-
|
214
|
+
if month.nil? && day.nil? && year_or_datestring.is_a?(String)
|
215
|
+
date = Date.parse(year_or_datestring)
|
216
|
+
set_date(query_string, date.year, date.month, date.day)
|
217
|
+
else
|
218
|
+
year = year_or_datestring
|
219
|
+
query(query_string, updateDate: [year, month-1, day])
|
220
|
+
end
|
221
|
+
end
|
199
222
|
|
200
|
-
|
223
|
+
def set_time(query_string, hour_or_timestring, minute=nil)
|
224
|
+
wait_for_element_exists(query_string)
|
201
225
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
226
|
+
if minute.nil? && hour_or_timestring.is_a?(String)
|
227
|
+
time = Time.parse(hour_or_timestring)
|
228
|
+
set_time(query_string, time.hour, time.min)
|
229
|
+
else
|
230
|
+
hour = hour_or_timestring
|
231
|
+
query(query_string, setCurrentHour: hour)
|
232
|
+
query(query_string, setCurrentMinute: minute)
|
233
|
+
end
|
234
|
+
end
|
208
235
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
puts "(Hooks are stored in features/support)"
|
213
|
-
end
|
236
|
+
def classes(query_string, *args)
|
237
|
+
query(query_string, :class, *args)
|
238
|
+
end
|
214
239
|
|
215
|
-
|
216
|
-
|
240
|
+
def ni
|
241
|
+
raise "Not yet implemented."
|
242
|
+
end
|
217
243
|
|
218
|
-
|
244
|
+
###
|
219
245
|
|
220
|
-
|
221
|
-
@serial = serial || default_serial
|
222
|
-
@server_port = server_port || default_server_port
|
223
|
-
@app_path = app_path
|
224
|
-
@test_server_path = test_server_path
|
225
|
-
@test_server_port = test_server_port
|
246
|
+
### simple page object helper
|
226
247
|
|
227
|
-
|
228
|
-
|
229
|
-
log `#{forward_cmd}`
|
248
|
+
def page(clz, *args)
|
249
|
+
clz.new(self, *args)
|
230
250
|
end
|
231
251
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
252
|
+
###
|
253
|
+
|
254
|
+
### app life cycle
|
255
|
+
def connect_to_test_server
|
256
|
+
puts "Explicit calls to connect_to_test_server should be removed."
|
257
|
+
puts "Please take a look in your hooks file for calls to this methods."
|
258
|
+
puts "(Hooks are stored in features/support)"
|
236
259
|
end
|
237
260
|
|
238
|
-
def
|
239
|
-
|
240
|
-
|
261
|
+
def disconnect_from_test_server
|
262
|
+
puts "Explicit calls to disconnect_from_test_server should be removed."
|
263
|
+
puts "Please take a look in your hooks file for calls to this methods."
|
264
|
+
puts "(Hooks are stored in features/support)"
|
241
265
|
end
|
242
266
|
|
243
|
-
|
244
|
-
|
245
|
-
log "Installing: #{app_path}"
|
246
|
-
result = `#{cmd}`
|
247
|
-
log result
|
248
|
-
pn = package_name(app_path)
|
249
|
-
succeeded = `#{adb_command} shell pm list packages`.include?("package:#{pn}")
|
267
|
+
class Device
|
268
|
+
attr_reader :app_path, :test_server_path, :serial, :server_port, :test_server_port
|
250
269
|
|
251
|
-
|
252
|
-
::Cucumber.wants_to_quit = true
|
253
|
-
raise "#{pn} did not get installed. Aborting!"
|
254
|
-
end
|
255
|
-
end
|
270
|
+
def initialize(cucumber_world, serial, server_port, app_path, test_server_path, test_server_port = 7102)
|
256
271
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
272
|
+
@cucumber_world = cucumber_world
|
273
|
+
@serial = serial || default_serial
|
274
|
+
@server_port = server_port || default_server_port
|
275
|
+
@app_path = app_path
|
276
|
+
@test_server_path = test_server_path
|
277
|
+
@test_server_port = test_server_port
|
263
278
|
|
264
|
-
|
265
|
-
|
266
|
-
|
279
|
+
forward_cmd = "#{adb_command} forward tcp:#{@server_port} tcp:#{@test_server_port}"
|
280
|
+
log forward_cmd
|
281
|
+
log `#{forward_cmd}`
|
267
282
|
end
|
268
|
-
end
|
269
|
-
|
270
|
-
def uninstall_app(package_name)
|
271
|
-
log "Uninstalling: #{package_name}"
|
272
|
-
log `#{adb_command} uninstall #{package_name}`
|
273
|
-
end
|
274
283
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
284
|
+
def reinstall_apps
|
285
|
+
uninstall_app(package_name(@app_path))
|
286
|
+
uninstall_app(package_name(@test_server_path))
|
287
|
+
install_app(@app_path)
|
288
|
+
install_app(@test_server_path)
|
280
289
|
end
|
281
|
-
end
|
282
290
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
end
|
291
|
+
def reinstall_test_server
|
292
|
+
uninstall_app(package_name(@test_server_path))
|
293
|
+
install_app(@test_server_path)
|
294
|
+
end
|
288
295
|
|
289
|
-
|
290
|
-
|
296
|
+
def install_app(app_path)
|
297
|
+
cmd = "#{adb_command} install \"#{app_path}\""
|
298
|
+
log "Installing: #{app_path}"
|
299
|
+
result = `#{cmd}`
|
300
|
+
log result
|
301
|
+
pn = package_name(app_path)
|
302
|
+
succeeded = `#{adb_command} shell pm list packages`.lines.map{|line| line.chomp.sub("package:", "")}.include?(pn)
|
303
|
+
|
304
|
+
unless succeeded
|
305
|
+
::Cucumber.wants_to_quit = true
|
306
|
+
raise "#{pn} did not get installed. Reason: '#{result.lines.last.chomp}'. Aborting!"
|
307
|
+
end
|
308
|
+
end
|
291
309
|
|
292
|
-
|
310
|
+
def update_app(app_path)
|
311
|
+
cmd = "#{adb_command} install -r \"#{app_path}\""
|
312
|
+
log "Updating: #{app_path}"
|
313
|
+
result = `#{cmd}`
|
314
|
+
log "result: #{result}"
|
315
|
+
succeeded = result.include?("Success")
|
293
316
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
rescue Exception => e
|
298
|
-
log "Error communicating with test server: #{e}"
|
299
|
-
raise e
|
300
|
-
end
|
301
|
-
log "Result:'" + result.strip + "'"
|
302
|
-
raise "Empty result from TestServer" if result.chomp.empty?
|
303
|
-
result = JSON.parse(result)
|
304
|
-
if not result["success"] then
|
305
|
-
raise "Action '#{action}' unsuccessful: #{result["message"]}"
|
317
|
+
unless succeeded
|
318
|
+
::Cucumber.wants_to_quit = true
|
319
|
+
raise "#{pn} did not get updated. Aborting!"
|
306
320
|
end
|
307
|
-
result
|
308
321
|
end
|
309
|
-
rescue Timeout::Error
|
310
|
-
raise Exception, "Step timed out"
|
311
|
-
end
|
312
322
|
|
313
|
-
|
314
|
-
|
323
|
+
def uninstall_app(package_name)
|
324
|
+
log "Uninstalling: #{package_name}"
|
325
|
+
log `#{adb_command} uninstall #{package_name}`
|
315
326
|
|
316
|
-
|
317
|
-
make_http_request(
|
318
|
-
:method => :post,
|
319
|
-
:body => data.to_json,
|
320
|
-
:uri => url_for(path),
|
321
|
-
:header => {"Content-Type" => "application/json;charset=utf-8"})
|
327
|
+
succeeded = !(`#{adb_command} shell pm list packages`.lines.map{|line| line.chomp.sub("package:", "")}.include?(package_name))
|
322
328
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
log "It looks like your app is no longer running. \nIt could be because of a crash or because your test script shut it down."
|
328
|
-
raise e
|
329
|
+
unless succeeded
|
330
|
+
::Cucumber.wants_to_quit = true
|
331
|
+
raise "#{package_name} was not uninstalled. Aborting!"
|
332
|
+
end
|
329
333
|
end
|
330
|
-
end
|
331
334
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
+
def app_running?
|
336
|
+
begin
|
337
|
+
http("/ping") == "pong"
|
338
|
+
rescue
|
339
|
+
false
|
340
|
+
end
|
341
|
+
end
|
335
342
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
path = "#{path}#{method}"
|
341
|
-
else
|
342
|
-
path = "#{path}/#{method}"
|
343
|
+
def keyguard_enabled?
|
344
|
+
dumpsys = `#{adb_command} shell dumpsys window windows`
|
345
|
+
#If a line containing mCurrentFocus and Keyguard exists the keyguard is enabled
|
346
|
+
dumpsys.lines.any? { |l| l.include?("mCurrentFocus") and l.include?("Keyguard")}
|
343
347
|
end
|
344
|
-
url.path = path
|
345
|
-
url
|
346
|
-
end
|
347
348
|
|
349
|
+
def perform_action(action, *arguments)
|
350
|
+
log "Action: #{action} - Params: #{arguments.join(', ')}"
|
351
|
+
|
352
|
+
params = {"command" => action, "arguments" => arguments}
|
353
|
+
|
354
|
+
Timeout.timeout(300) do
|
355
|
+
begin
|
356
|
+
result = http("/", params, {:read_timeout => 350})
|
357
|
+
rescue Exception => e
|
358
|
+
log "Error communicating with test server: #{e}"
|
359
|
+
raise e
|
360
|
+
end
|
361
|
+
log "Result:'" + result.strip + "'"
|
362
|
+
raise "Empty result from TestServer" if result.chomp.empty?
|
363
|
+
result = JSON.parse(result)
|
364
|
+
if not result["success"] then
|
365
|
+
raise "Action '#{action}' unsuccessful: #{result["message"]}"
|
366
|
+
end
|
367
|
+
result
|
368
|
+
end
|
369
|
+
rescue Timeout::Error
|
370
|
+
raise Exception, "Step timed out"
|
371
|
+
end
|
348
372
|
|
373
|
+
def http(path, data = {}, options = {})
|
374
|
+
begin
|
349
375
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
376
|
+
configure_http(@http, options)
|
377
|
+
make_http_request(
|
378
|
+
:method => :post,
|
379
|
+
:body => data.to_json,
|
380
|
+
:uri => url_for(path),
|
381
|
+
:header => {"Content-Type" => "application/json;charset=utf-8"})
|
382
|
+
|
383
|
+
rescue HTTPClient::TimeoutError,
|
384
|
+
HTTPClient::KeepAliveDisconnected,
|
385
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED,
|
386
|
+
Errno::ETIMEDOUT => e
|
387
|
+
log "It looks like your app is no longer running. \nIt could be because of a crash or because your test script shut it down."
|
388
|
+
raise e
|
354
389
|
end
|
355
|
-
|
356
|
-
header["Content-Type"] = "application/json;charset=utf-8"
|
357
|
-
options[:header] = header
|
390
|
+
end
|
358
391
|
|
392
|
+
def set_http(http)
|
393
|
+
@http = http
|
394
|
+
end
|
359
395
|
|
360
|
-
|
361
|
-
|
396
|
+
def url_for(method)
|
397
|
+
url = URI.parse(ENV['DEVICE_ENDPOINT']|| "http://127.0.0.1:#{@server_port}")
|
398
|
+
path = url.path
|
399
|
+
if path.end_with? "/"
|
400
|
+
path = "#{path}#{method}"
|
362
401
|
else
|
363
|
-
|
402
|
+
path = "#{path}/#{method}"
|
364
403
|
end
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
404
|
+
url.path = path
|
405
|
+
url
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
|
410
|
+
def make_http_request(options)
|
411
|
+
begin
|
412
|
+
unless @http
|
413
|
+
@http = init_request(options)
|
414
|
+
end
|
415
|
+
header = options[:header] || {}
|
416
|
+
header["Content-Type"] = "application/json;charset=utf-8"
|
417
|
+
options[:header] = header
|
418
|
+
|
419
|
+
|
420
|
+
response = if options[:method] == :post
|
421
|
+
@http.post(options[:uri], options)
|
422
|
+
else
|
423
|
+
@http.get(options[:uri], options)
|
424
|
+
end
|
425
|
+
raise Errno::ECONNREFUSED if response.status_code == 502
|
426
|
+
response.body
|
427
|
+
rescue Exception => e
|
428
|
+
if @http
|
429
|
+
@http.reset_all
|
430
|
+
@http=nil
|
431
|
+
end
|
432
|
+
raise e
|
371
433
|
end
|
372
|
-
raise e
|
373
434
|
end
|
374
|
-
end
|
375
435
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
436
|
+
def init_request(options)
|
437
|
+
http = HTTPClient.new
|
438
|
+
configure_http(http, options)
|
439
|
+
end
|
380
440
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
else
|
389
|
-
if ENV['DEBUG_HTTP'] and (ENV['DEBUG_HTTP'] != '0')
|
390
|
-
http.debug_dev = $stdout
|
441
|
+
def configure_http(http, options)
|
442
|
+
return unless http
|
443
|
+
http.connect_timeout = options[:open_timeout] || 15
|
444
|
+
http.send_timeout = options[:send_timeout] || 15
|
445
|
+
http.receive_timeout = options[:read_timeout] || 15
|
446
|
+
if options.has_key?(:debug) && options[:debug]
|
447
|
+
http.debug_dev= $stdout
|
391
448
|
else
|
392
|
-
|
449
|
+
if ENV['DEBUG_HTTP'] and (ENV['DEBUG_HTTP'] != '0')
|
450
|
+
http.debug_dev = $stdout
|
451
|
+
else
|
452
|
+
http.debug_dev= nil
|
453
|
+
end
|
393
454
|
end
|
455
|
+
http
|
394
456
|
end
|
395
|
-
http
|
396
|
-
end
|
397
457
|
|
398
|
-
|
399
|
-
|
400
|
-
|
458
|
+
def screenshot(options={:prefix => nil, :name => nil})
|
459
|
+
prefix = options[:prefix] || ENV['SCREENSHOT_PATH'] || ""
|
460
|
+
name = options[:name]
|
401
461
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
462
|
+
if name.nil?
|
463
|
+
name = "screenshot"
|
464
|
+
else
|
465
|
+
if File.extname(name).downcase == ".png"
|
466
|
+
name = name.split(".png")[0]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
@@screenshot_count ||= 0
|
471
|
+
path = "#{prefix}#{name}_#{@@screenshot_count}.png"
|
472
|
+
|
473
|
+
if ENV["SCREENSHOT_VIA_USB"] == "false"
|
474
|
+
begin
|
475
|
+
res = http("/screenshot")
|
476
|
+
rescue EOFError
|
477
|
+
raise "Could not take screenshot. App is most likely not running anymore."
|
478
|
+
end
|
479
|
+
File.open(path, 'wb') do |f|
|
480
|
+
f.write res
|
481
|
+
end
|
482
|
+
else
|
483
|
+
screenshot_cmd = "java -jar #{File.join(File.dirname(__FILE__), 'lib', 'screenshotTaker.jar')} #{serial} \"#{path}\""
|
484
|
+
log screenshot_cmd
|
485
|
+
raise "Could not take screenshot" unless system(screenshot_cmd)
|
407
486
|
end
|
487
|
+
|
488
|
+
@@screenshot_count += 1
|
489
|
+
path
|
408
490
|
end
|
409
491
|
|
410
|
-
|
411
|
-
|
492
|
+
def client_version
|
493
|
+
Calabash::Android::VERSION
|
494
|
+
end
|
412
495
|
|
413
|
-
|
496
|
+
def server_version
|
414
497
|
begin
|
415
|
-
|
416
|
-
|
417
|
-
|
498
|
+
response = perform_action('version')
|
499
|
+
raise 'Invalid response' unless response['success']
|
500
|
+
rescue => e
|
501
|
+
log("Could not contact server")
|
502
|
+
log(e && e.backtrace && e.backtrace.join("\n"))
|
503
|
+
raise "The server did not respond. Make sure the server is running."
|
418
504
|
end
|
419
|
-
File.open(path, 'wb') do |f|
|
420
|
-
f.write res
|
421
|
-
end
|
422
|
-
else
|
423
|
-
screenshot_cmd = "java -jar #{File.join(File.dirname(__FILE__), 'lib', 'screenshotTaker.jar')} #{serial} \"#{path}\""
|
424
|
-
log screenshot_cmd
|
425
|
-
raise "Could not take screenshot" unless system(screenshot_cmd)
|
426
|
-
end
|
427
505
|
|
428
|
-
|
429
|
-
|
430
|
-
end
|
506
|
+
response['message']
|
507
|
+
end
|
431
508
|
|
432
|
-
|
433
|
-
|
434
|
-
|
509
|
+
def adb_command
|
510
|
+
"#{Env.adb_path} -s #{serial}"
|
511
|
+
end
|
435
512
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
513
|
+
def default_serial
|
514
|
+
devices = connected_devices
|
515
|
+
log "connected_devices: #{devices}"
|
516
|
+
raise "No connected devices" if devices.empty?
|
517
|
+
raise "More than one device connected. Specify device serial using ADB_DEVICE_ARG" if devices.length > 1
|
518
|
+
devices.first
|
519
|
+
end
|
443
520
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
521
|
+
def default_server_port
|
522
|
+
require 'yaml'
|
523
|
+
File.open(File.expand_path(server_port_configuration), File::RDWR|File::CREAT) do |f|
|
524
|
+
f.flock(File::LOCK_EX)
|
525
|
+
state = YAML::load(f) || {}
|
526
|
+
ports = state['server_ports'] ||= {}
|
527
|
+
return ports[serial] if ports.has_key?(serial)
|
451
528
|
|
452
|
-
|
453
|
-
|
454
|
-
|
529
|
+
port = 34777
|
530
|
+
port += 1 while ports.has_value?(port)
|
531
|
+
ports[serial] = port
|
455
532
|
|
456
|
-
|
457
|
-
|
458
|
-
|
533
|
+
f.rewind
|
534
|
+
f.write(YAML::dump(state))
|
535
|
+
f.truncate(f.pos)
|
459
536
|
|
460
|
-
|
461
|
-
|
537
|
+
log "Persistently allocated port #{port} to #{serial}"
|
538
|
+
return port
|
539
|
+
end
|
462
540
|
end
|
463
|
-
end
|
464
541
|
|
465
|
-
|
466
|
-
|
467
|
-
|
542
|
+
def server_port_configuration
|
543
|
+
File.expand_path(ENV['CALABASH_SERVER_PORTS'] || "~/.calabash.yaml")
|
544
|
+
end
|
468
545
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
546
|
+
def connected_devices
|
547
|
+
lines = `#{Env.adb_path} devices`.split("\n")
|
548
|
+
start_index = lines.index{ |x| x =~ /List of devices attached/ } + 1
|
549
|
+
lines[start_index..-1].collect { |l| l.split("\t").first }
|
550
|
+
end
|
474
551
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
552
|
+
def wake_up
|
553
|
+
wake_up_cmd = "#{adb_command} shell am start -a android.intent.action.MAIN -n #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.WakeUp"
|
554
|
+
log "Waking up device using:"
|
555
|
+
log wake_up_cmd
|
556
|
+
raise "Could not wake up the device" unless system(wake_up_cmd)
|
480
557
|
|
481
|
-
|
482
|
-
|
558
|
+
retriable :tries => 10, :interval => 1 do
|
559
|
+
raise "Could not remove the keyguard" if keyguard_enabled?
|
560
|
+
end
|
483
561
|
end
|
484
|
-
end
|
485
562
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
563
|
+
def clear_app_data
|
564
|
+
cmd = "#{adb_command} shell am instrument #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.ClearAppData"
|
565
|
+
raise "Could not clear data" unless system(cmd)
|
566
|
+
end
|
490
567
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
568
|
+
def pull(remote, local)
|
569
|
+
cmd = "#{adb_command} pull #{remote} #{local}"
|
570
|
+
raise "Could not pull #{remote} to #{local}" unless system(cmd)
|
571
|
+
end
|
495
572
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
573
|
+
def push(local, remote)
|
574
|
+
cmd = "#{adb_command} push #{local} #{remote}"
|
575
|
+
raise "Could not push #{local} to #{remote}" unless system(cmd)
|
576
|
+
end
|
500
577
|
|
501
|
-
|
502
|
-
|
578
|
+
def start_test_server_in_background(options={})
|
579
|
+
raise "Will not start test server because of previous failures." if ::Cucumber.wants_to_quit
|
503
580
|
|
504
|
-
|
505
|
-
|
506
|
-
|
581
|
+
if keyguard_enabled?
|
582
|
+
wake_up
|
583
|
+
end
|
507
584
|
|
508
|
-
|
509
|
-
:main_activity => main_activity(@app_path),
|
510
|
-
:test_server_port => @test_server_port,
|
511
|
-
:class => "sh.calaba.instrumentationbackend.InstrumentationBackend"}
|
585
|
+
env_options = options
|
512
586
|
|
513
|
-
|
587
|
+
env_options[:target_package] ||= package_name(@app_path)
|
588
|
+
env_options[:main_activity] ||= main_activity(@app_path)
|
589
|
+
env_options[:test_server_port] ||= @test_server_port
|
590
|
+
env_options[:class] ||= "sh.calaba.instrumentationbackend.InstrumentationBackend"
|
514
591
|
|
515
|
-
|
592
|
+
cmd_arr = [adb_command, "shell am instrument"]
|
516
593
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
594
|
+
env_options.each_pair do |key, val|
|
595
|
+
cmd_arr << "-e"
|
596
|
+
cmd_arr << key.to_s
|
597
|
+
cmd_arr << val.to_s
|
598
|
+
end
|
522
599
|
|
523
|
-
|
600
|
+
cmd_arr << "#{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner"
|
524
601
|
|
525
|
-
|
602
|
+
cmd = cmd_arr.join(" ")
|
526
603
|
|
527
|
-
|
528
|
-
|
529
|
-
|
604
|
+
log "Starting test server using:"
|
605
|
+
log cmd
|
606
|
+
raise "Could not execute command to start test server" unless system("#{cmd} 2>&1")
|
530
607
|
|
531
|
-
|
532
|
-
|
533
|
-
|
608
|
+
retriable :tries => 10, :interval => 1 do
|
609
|
+
raise "App did not start" unless app_running?
|
610
|
+
end
|
534
611
|
|
535
|
-
|
536
|
-
|
612
|
+
begin
|
613
|
+
retriable :tries => 10, :interval => 3 do
|
537
614
|
log "Checking if instrumentation backend is ready"
|
538
615
|
|
539
616
|
log "Is app running? #{app_running?}"
|
@@ -544,450 +621,567 @@ module Operations
|
|
544
621
|
else
|
545
622
|
log "Instrumentation backend is ready!"
|
546
623
|
end
|
547
|
-
|
548
|
-
|
624
|
+
end
|
625
|
+
rescue Exception => e
|
549
626
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
627
|
+
msg = "Unable to make connection to Calabash Test Server at http://127.0.0.1:#{@server_port}/\n"
|
628
|
+
msg << "Please check the logcat output for more info about what happened\n"
|
629
|
+
raise msg
|
630
|
+
end
|
554
631
|
|
555
|
-
|
556
|
-
response = perform_action('version')
|
557
|
-
unless response['success']
|
558
|
-
msg = ["Unable to obtain Test Server version. "]
|
559
|
-
msg << "Please run 'reinstall_test_server' to make sure you have the correct version"
|
560
|
-
msg_s = msg.join("\n")
|
561
|
-
log(msg_s)
|
562
|
-
raise msg_s
|
563
|
-
end
|
564
|
-
unless response['message'] == Calabash::Android::VERSION
|
632
|
+
log "Checking client-server version match..."
|
565
633
|
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
end
|
576
|
-
log("Client and server versions match. Proceeding...")
|
634
|
+
begin
|
635
|
+
server_version = server_version()
|
636
|
+
rescue
|
637
|
+
msg = ["Unable to obtain Test Server version. "]
|
638
|
+
msg << "Please run 'reinstall_test_server' to make sure you have the correct version"
|
639
|
+
msg_s = msg.join("\n")
|
640
|
+
log(msg_s)
|
641
|
+
raise msg_s
|
642
|
+
end
|
577
643
|
|
644
|
+
client_version = client_version()
|
645
|
+
|
646
|
+
unless server_version == client_version
|
647
|
+
msg = ["Calabash Client and Test-server version mismatch."]
|
648
|
+
msg << "Client version #{client_version}"
|
649
|
+
msg << "Test-server version #{server_version}"
|
650
|
+
msg << "Expected Test-server version #{client_version}"
|
651
|
+
msg << "\n\nSolution:\n\n"
|
652
|
+
msg << "Run 'reinstall_test_server' to make sure you have the correct version"
|
653
|
+
msg_s = msg.join("\n")
|
654
|
+
log(msg_s)
|
655
|
+
raise msg_s
|
656
|
+
end
|
578
657
|
|
579
|
-
|
658
|
+
log("Client and server versions match (client: #{client_version}, server: #{server_version}). Proceeding...")
|
659
|
+
end
|
580
660
|
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
661
|
+
def shutdown_test_server
|
662
|
+
begin
|
663
|
+
http("/kill")
|
664
|
+
Timeout::timeout(3) do
|
665
|
+
sleep 0.3 while app_running?
|
666
|
+
end
|
667
|
+
rescue HTTPClient::KeepAliveDisconnected
|
668
|
+
log ("Server not responding. Moving on.")
|
669
|
+
rescue Timeout::Error
|
670
|
+
log ("Could not kill app. Waited to 3 seconds.")
|
671
|
+
rescue EOFError
|
672
|
+
log ("Could not kill app. App is most likely not running anymore.")
|
586
673
|
end
|
587
|
-
rescue HTTPClient::KeepAliveDisconnected
|
588
|
-
log ("Server not responding. Moving on.")
|
589
|
-
rescue Timeout::Error
|
590
|
-
log ("Could not kill app. Waited to 3 seconds.")
|
591
|
-
rescue EOFError
|
592
|
-
log ("Could not kill app. App is most likely not running anymore.")
|
593
674
|
end
|
594
|
-
end
|
595
675
|
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
676
|
+
##location
|
677
|
+
def set_gps_coordinates_from_location(location)
|
678
|
+
require 'geocoder'
|
679
|
+
results = Geocoder.search(location)
|
680
|
+
raise Exception, "Got no results for #{location}" if results.empty?
|
601
681
|
|
602
|
-
|
603
|
-
|
604
|
-
|
682
|
+
best_result = results.first
|
683
|
+
set_gps_coordinates(best_result.latitude, best_result.longitude)
|
684
|
+
end
|
605
685
|
|
606
|
-
|
607
|
-
|
608
|
-
|
686
|
+
def set_gps_coordinates(latitude, longitude)
|
687
|
+
perform_action('set_gps_coordinates', latitude, longitude)
|
688
|
+
end
|
609
689
|
|
610
|
-
|
690
|
+
def get_preferences(name)
|
611
691
|
|
612
|
-
|
613
|
-
|
692
|
+
log "Get preferences: #{name}, app running? #{app_running?}"
|
693
|
+
preferences = {}
|
614
694
|
|
615
|
-
|
616
|
-
|
617
|
-
|
695
|
+
if app_running?
|
696
|
+
json = perform_action('get_preferences', name);
|
697
|
+
else
|
618
698
|
|
619
|
-
|
620
|
-
|
699
|
+
logcat_id = get_logcat_id()
|
700
|
+
cmd = "#{adb_command} shell am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.GetPreferences"
|
621
701
|
|
622
|
-
|
702
|
+
raise "Could not get preferences" unless system(cmd)
|
623
703
|
|
624
|
-
|
625
|
-
|
704
|
+
logcat_cmd = get_logcat_cmd(logcat_id)
|
705
|
+
logcat_output = `#{logcat_cmd}`
|
626
706
|
|
627
|
-
|
707
|
+
json = get_json_from_logcat(logcat_output)
|
628
708
|
|
629
|
-
|
630
|
-
|
709
|
+
raise "Could not get preferences" unless json != nil and json["success"]
|
710
|
+
end
|
631
711
|
|
632
|
-
|
633
|
-
|
634
|
-
|
712
|
+
# at this point we have valid json, coming from an action
|
713
|
+
# or instrumentation, but we don't care, just parse
|
714
|
+
if json["bonusInformation"].length > 0
|
635
715
|
json["bonusInformation"].each do |item|
|
636
|
-
|
637
|
-
|
716
|
+
json_item = JSON.parse(item)
|
717
|
+
preferences[json_item["key"]] = json_item["value"]
|
718
|
+
end
|
638
719
|
end
|
720
|
+
|
721
|
+
preferences
|
639
722
|
end
|
640
723
|
|
641
|
-
|
642
|
-
end
|
724
|
+
def set_preferences(name, hash)
|
643
725
|
|
644
|
-
|
726
|
+
log "Set preferences: #{name}, #{hash}, app running? #{app_running?}"
|
645
727
|
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
perform_action('set_preferences', name, hash);
|
650
|
-
else
|
728
|
+
if app_running?
|
729
|
+
perform_action('set_preferences', name, hash);
|
730
|
+
else
|
651
731
|
|
652
|
-
|
732
|
+
params = hash.map {|k,v| "-e \"#{k}\" \"#{v}\""}.join(" ")
|
653
733
|
|
654
|
-
|
655
|
-
|
734
|
+
logcat_id = get_logcat_id()
|
735
|
+
cmd = "#{adb_command} shell am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{params} #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.SetPreferences"
|
656
736
|
|
657
|
-
|
737
|
+
raise "Could not set preferences" unless system(cmd)
|
658
738
|
|
659
|
-
|
660
|
-
|
739
|
+
logcat_cmd = get_logcat_cmd(logcat_id)
|
740
|
+
logcat_output = `#{logcat_cmd}`
|
661
741
|
|
662
|
-
|
742
|
+
json = get_json_from_logcat(logcat_output)
|
663
743
|
|
664
|
-
|
744
|
+
raise "Could not set preferences" unless json != nil and json["success"]
|
745
|
+
end
|
665
746
|
end
|
666
|
-
end
|
667
747
|
|
668
|
-
|
748
|
+
def clear_preferences(name)
|
669
749
|
|
670
|
-
|
750
|
+
log "Clear preferences: #{name}, app running? #{app_running?}"
|
671
751
|
|
672
|
-
|
673
|
-
|
674
|
-
|
752
|
+
if app_running?
|
753
|
+
perform_action('clear_preferences', name);
|
754
|
+
else
|
675
755
|
|
676
|
-
|
677
|
-
|
678
|
-
|
756
|
+
logcat_id = get_logcat_id()
|
757
|
+
cmd = "#{adb_command} shell am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.ClearPreferences"
|
758
|
+
raise "Could not clear preferences" unless system(cmd)
|
679
759
|
|
680
|
-
|
681
|
-
|
760
|
+
logcat_cmd = get_logcat_cmd(logcat_id)
|
761
|
+
logcat_output = `#{logcat_cmd}`
|
682
762
|
|
683
|
-
|
763
|
+
json = get_json_from_logcat(logcat_output)
|
684
764
|
|
685
|
-
|
765
|
+
raise "Could not clear preferences" unless json != nil and json["success"]
|
766
|
+
end
|
686
767
|
end
|
687
|
-
end
|
688
768
|
|
689
|
-
|
769
|
+
def get_json_from_logcat(logcat_output)
|
690
770
|
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
771
|
+
logcat_output.split(/\r?\n/).each do |line|
|
772
|
+
begin
|
773
|
+
json = JSON.parse(line)
|
774
|
+
return json
|
775
|
+
rescue
|
776
|
+
# nothing to do here, just discarding logcat rubbish
|
777
|
+
end
|
697
778
|
end
|
779
|
+
|
780
|
+
return nil
|
781
|
+
end
|
782
|
+
|
783
|
+
def get_logcat_id()
|
784
|
+
# we need a unique logcat tag so we can later
|
785
|
+
# query the logcat output and filter out everything
|
786
|
+
# but what we are interested in
|
787
|
+
|
788
|
+
random = (0..10000).to_a.sample
|
789
|
+
"#{Time.now.strftime("%s")}_#{random}"
|
790
|
+
end
|
791
|
+
|
792
|
+
def get_logcat_cmd(tag)
|
793
|
+
# returns raw logcat output for our tag
|
794
|
+
# filtering out everthing else
|
795
|
+
|
796
|
+
"#{adb_command} logcat -d -v raw #{tag}:* *:S"
|
698
797
|
end
|
798
|
+
end
|
699
799
|
|
700
|
-
|
800
|
+
def label(uiquery)
|
801
|
+
ni
|
701
802
|
end
|
702
803
|
|
703
|
-
def
|
704
|
-
|
705
|
-
|
706
|
-
|
804
|
+
def screenshot_and_raise(msg, options = nil)
|
805
|
+
if options
|
806
|
+
screenshot_embed options
|
807
|
+
else
|
808
|
+
screenshot_embed
|
809
|
+
end
|
810
|
+
raise(msg)
|
811
|
+
end
|
707
812
|
|
708
|
-
|
709
|
-
|
813
|
+
def hide_soft_keyboard
|
814
|
+
perform_action('hide_soft_keyboard')
|
710
815
|
end
|
711
816
|
|
712
|
-
def
|
713
|
-
|
714
|
-
|
817
|
+
def execute_uiquery(uiquery)
|
818
|
+
if uiquery.instance_of? String
|
819
|
+
elements = query(uiquery)
|
820
|
+
|
821
|
+
return elements.first unless elements.empty?
|
822
|
+
else
|
823
|
+
elements = uiquery
|
824
|
+
|
825
|
+
return elements.first if elements.instance_of?(Array)
|
826
|
+
return elements if elements.instance_of?(Hash)
|
827
|
+
end
|
715
828
|
|
716
|
-
|
829
|
+
nil
|
717
830
|
end
|
718
|
-
end
|
719
831
|
|
720
|
-
|
721
|
-
|
722
|
-
|
832
|
+
def step_deprecated
|
833
|
+
puts 'Warning: This predefined step is deprecated.'
|
834
|
+
end
|
723
835
|
|
724
|
-
|
725
|
-
|
726
|
-
screenshot_embed options
|
727
|
-
else
|
728
|
-
screenshot_embed
|
836
|
+
def http(path, data = {}, options = {})
|
837
|
+
default_device.http(path, data, options)
|
729
838
|
end
|
730
|
-
raise(msg)
|
731
|
-
end
|
732
839
|
|
733
|
-
|
734
|
-
|
735
|
-
|
840
|
+
def html(q)
|
841
|
+
query(q).map {|e| e['html']}
|
842
|
+
end
|
736
843
|
|
737
|
-
|
738
|
-
|
739
|
-
|
844
|
+
def set_text(uiquery, txt)
|
845
|
+
puts "set_text is deprecated. Use enter_text instead"
|
846
|
+
enter_text(uiquery, txt)
|
847
|
+
end
|
740
848
|
|
741
|
-
|
742
|
-
|
743
|
-
|
849
|
+
def press_user_action_button(action_name=nil)
|
850
|
+
if action_name.nil?
|
851
|
+
perform_action("press_user_action_button")
|
852
|
+
else
|
853
|
+
perform_action("press_user_action_button", action_name)
|
854
|
+
end
|
855
|
+
end
|
744
856
|
|
745
|
-
|
746
|
-
|
857
|
+
def press_button(key)
|
858
|
+
perform_action('press_key', key)
|
747
859
|
end
|
748
860
|
|
749
|
-
|
750
|
-
|
861
|
+
def press_back_button
|
862
|
+
press_button('KEYCODE_BACK')
|
863
|
+
end
|
751
864
|
|
752
|
-
|
753
|
-
|
754
|
-
|
865
|
+
def press_menu_button
|
866
|
+
press_button('KEYCODE_MENU')
|
867
|
+
end
|
755
868
|
|
756
|
-
|
757
|
-
|
758
|
-
|
869
|
+
def press_down_button
|
870
|
+
press_button('KEYCODE_DPAD_DOWN')
|
871
|
+
end
|
759
872
|
|
760
|
-
|
761
|
-
|
762
|
-
|
873
|
+
def press_up_button
|
874
|
+
press_button('KEYCODE_DPAD_UP')
|
875
|
+
end
|
763
876
|
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
end
|
877
|
+
def press_left_button
|
878
|
+
press_button('KEYCODE_DPAD_LEFT')
|
879
|
+
end
|
768
880
|
|
769
|
-
|
770
|
-
|
771
|
-
|
881
|
+
def press_right_button
|
882
|
+
press_button('KEYCODE_DPAD_RIGHT')
|
883
|
+
end
|
772
884
|
|
773
|
-
|
774
|
-
|
775
|
-
|
885
|
+
def press_enter_button
|
886
|
+
press_button('KEYCODE_ENTER')
|
887
|
+
end
|
776
888
|
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
889
|
+
def select_options_menu_item(identifier, options={})
|
890
|
+
press_menu_button
|
891
|
+
tap_when_element_exists("DropDownListView * marked:'#{identifier}'", options)
|
892
|
+
end
|
781
893
|
|
782
|
-
|
783
|
-
|
894
|
+
def select_context_menu_item(view_uiquery, menu_item_query_string)
|
895
|
+
long_press(view_uiquery)
|
784
896
|
|
785
|
-
|
786
|
-
|
897
|
+
container_class = 'com.android.internal.view.menu.ListMenuItemView'
|
898
|
+
wait_for_element_exists(container_class)
|
787
899
|
|
788
|
-
|
789
|
-
|
790
|
-
|
900
|
+
combined_query_string = "#{container_class} descendant #{menu_item_query_string}"
|
901
|
+
touch(combined_query_string)
|
902
|
+
end
|
791
903
|
|
792
|
-
|
793
|
-
|
794
|
-
|
904
|
+
def select_item_from_spinner(item_query_string, options={})
|
905
|
+
spinner_query_string = options[:spinner] || "android.widget.AbsSpinner"
|
906
|
+
direction = options[:direction] || :down
|
907
|
+
count = query(spinner_query_string, :getCount).first
|
908
|
+
scroll_view_query_string = options[:scroll_view] || "android.widget.AbsListView index:0"
|
795
909
|
|
796
|
-
|
797
|
-
|
798
|
-
|
910
|
+
unless direction == :up || direction == :down
|
911
|
+
raise "Invalid direction '#{direction}'. Only upwards and downwards scrolling is supported"
|
912
|
+
end
|
799
913
|
|
800
|
-
|
801
|
-
|
802
|
-
|
914
|
+
touch(spinner_query_string)
|
915
|
+
|
916
|
+
change_direction = false
|
917
|
+
|
918
|
+
wait_for({retry_frequency: 0}.merge(options)) do
|
919
|
+
if query(item_query_string).empty?
|
920
|
+
scroll(scroll_view_query_string, direction)
|
921
|
+
|
922
|
+
if change_direction
|
923
|
+
direction = direction == :up ? :down : :up
|
924
|
+
change_direction = false
|
925
|
+
else
|
926
|
+
# Because getLastVisiblePosition returns the last element even though it is not visible,
|
927
|
+
# we have to scroll one more time to make sure we do not change direction before the last
|
928
|
+
# element is fully visible
|
929
|
+
if direction == :down
|
930
|
+
change_direction = true if query(scroll_view_query_string, :getLastVisiblePosition).first+1 == count
|
931
|
+
elsif direction == :up
|
932
|
+
change_direction = true if query(scroll_view_query_string, :getFirstVisiblePosition).first == 0
|
933
|
+
end
|
934
|
+
end
|
803
935
|
|
804
|
-
|
805
|
-
|
806
|
-
|
936
|
+
false
|
937
|
+
else
|
938
|
+
true
|
939
|
+
end
|
940
|
+
end
|
807
941
|
|
808
|
-
|
809
|
-
|
810
|
-
end
|
942
|
+
touch(item_query_string)
|
943
|
+
end
|
811
944
|
|
812
|
-
|
813
|
-
|
814
|
-
raise 'Only upwards and downwards scrolling is supported for now'
|
945
|
+
def swipe(query_string, options={})
|
946
|
+
raise 'Swipe not implemented. Use flick or pan instead.'
|
815
947
|
end
|
816
948
|
|
817
|
-
|
818
|
-
|
949
|
+
def cell_swipe(options={})
|
950
|
+
ni
|
951
|
+
end
|
819
952
|
|
820
|
-
|
821
|
-
|
822
|
-
|
953
|
+
def done
|
954
|
+
ni
|
955
|
+
end
|
823
956
|
|
824
|
-
|
825
|
-
|
957
|
+
def find_scrollable_view(options={})
|
958
|
+
timeout = options[:timeout] || 30
|
826
959
|
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
960
|
+
begin
|
961
|
+
Timeout.timeout(timeout, WaitError) do
|
962
|
+
scroll_view_query_string = "android.widget.ScrollView index:0"
|
963
|
+
list_view_query_string = "android.widget.AbsListView index:0"
|
964
|
+
web_view_query_string = "android.webkit.WebView index:0"
|
965
|
+
|
966
|
+
loop do
|
967
|
+
if element_exists(scroll_view_query_string)
|
968
|
+
return scroll_view_query_string
|
969
|
+
elsif element_exists(list_view_query_string)
|
970
|
+
return list_view_query_string
|
971
|
+
elsif element_exists(web_view_query_string)
|
972
|
+
return web_view_query_string
|
973
|
+
end
|
974
|
+
end
|
975
|
+
end
|
976
|
+
rescue WaitError
|
977
|
+
raise WaitError.new('Could not find any scrollable views')
|
831
978
|
end
|
979
|
+
end
|
832
980
|
|
833
|
-
|
981
|
+
def scroll_up(options={})
|
982
|
+
scroll(find_scrollable_view(options), :up)
|
834
983
|
end
|
835
984
|
|
836
|
-
|
837
|
-
|
985
|
+
def scroll_down(options={})
|
986
|
+
scroll(find_scrollable_view(options), :down)
|
987
|
+
end
|
838
988
|
|
839
|
-
|
840
|
-
|
989
|
+
def scroll(query_string, direction)
|
990
|
+
if direction != :up && direction != :down
|
991
|
+
raise 'Only upwards and downwards scrolling is supported for now'
|
992
|
+
end
|
841
993
|
|
842
|
-
|
994
|
+
action = lambda do
|
995
|
+
elements = query(query_string)
|
996
|
+
raise "No elements found. Query: #{query_string}" if elements.empty?
|
843
997
|
|
844
|
-
|
845
|
-
|
846
|
-
|
998
|
+
if elements.length > 1
|
999
|
+
query_string = "#{query_string} index:0"
|
1000
|
+
end
|
847
1001
|
|
848
|
-
|
1002
|
+
element = elements.first
|
849
1003
|
|
850
|
-
|
1004
|
+
response = query(query_string, :getFirstVisiblePosition).first
|
851
1005
|
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
1006
|
+
if response.is_a?(Hash) && response.has_key?("error") # View is not of type android.widget.AbsListView
|
1007
|
+
scroll_x = 0
|
1008
|
+
scroll_y = 0
|
1009
|
+
width = element['rect']['width']
|
1010
|
+
height = element['rect']['height']
|
1011
|
+
|
1012
|
+
if direction == :up
|
1013
|
+
scroll_y = -height/2
|
1014
|
+
else
|
1015
|
+
scroll_y = height/2
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
query(query_string, {scrollBy: [scroll_x.to_i, scroll_y.to_i]})
|
1019
|
+
else # View is of type android.widget.AbsListView
|
1020
|
+
first_position = response.to_i
|
1021
|
+
last_position = query(query_string, :getLastVisiblePosition).first.to_i
|
1022
|
+
|
1023
|
+
selection_index = if direction == :up
|
1024
|
+
[first_position + [first_position - last_position + 1, -1].min, 0].max
|
1025
|
+
elsif direction == :down
|
1026
|
+
first_position + [last_position - first_position, 1].max
|
1027
|
+
end
|
856
1028
|
|
857
|
-
|
858
|
-
|
859
|
-
|
1029
|
+
query(query_string, setSelection: selection_index)
|
1030
|
+
end
|
1031
|
+
end
|
860
1032
|
|
861
|
-
|
862
|
-
scroll_view_query_string = element['webView']
|
863
|
-
else
|
864
|
-
scroll_view_query_string = "#{all_query_string} parent android.widget.ScrollView index:0"
|
1033
|
+
when_element_exists(query_string, action: action)
|
865
1034
|
end
|
866
1035
|
|
867
|
-
|
1036
|
+
def scroll_to(query_string, options={})
|
1037
|
+
options[:action] ||= lambda {}
|
868
1038
|
|
869
|
-
|
1039
|
+
all_query_string = query_string
|
870
1040
|
|
871
|
-
|
872
|
-
|
1041
|
+
unless all_query_string.chomp.downcase.start_with?('all')
|
1042
|
+
all_query_string = "all #{all_query_string}"
|
1043
|
+
end
|
873
1044
|
|
874
|
-
|
875
|
-
scroll_by_y = element_center_y - (scroll_element_y + scroll_element_height) + 2
|
876
|
-
else
|
877
|
-
scroll_by_y = element_center_y - scroll_element_y - 2
|
878
|
-
end
|
1045
|
+
wait_for_element_exists(all_query_string)
|
879
1046
|
|
880
|
-
|
881
|
-
|
1047
|
+
element = query(all_query_string).first
|
1048
|
+
raise "No elements found. Query: #{all_query_string}" if element.nil?
|
1049
|
+
element_y = element['rect']['y']
|
1050
|
+
element_height = element['rect']['height']
|
1051
|
+
element_bottom = element_y + element_height
|
882
1052
|
|
883
|
-
|
884
|
-
|
885
|
-
|
1053
|
+
scroll_view_query_string = options[:container] || if element.has_key?('html')
|
1054
|
+
"android.webkit.WebView id:'#{element['webView']}'"
|
1055
|
+
else
|
1056
|
+
"#{all_query_string} parent android.widget.ScrollView index:0"
|
1057
|
+
end
|
886
1058
|
|
887
|
-
|
888
|
-
query(uiquery, {:smoothScrollToPosition => number})
|
889
|
-
puts "TODO:detect end of scroll - use sleep for now"
|
890
|
-
end
|
1059
|
+
scroll_element = query(scroll_view_query_string).first
|
891
1060
|
|
892
|
-
|
893
|
-
ni
|
894
|
-
end
|
1061
|
+
raise "Could not find parent scroll view. Query: '#{escape_quotes(scroll_view_query_string)}'" if scroll_element.nil?
|
895
1062
|
|
896
|
-
|
897
|
-
|
898
|
-
end
|
1063
|
+
scroll_element_y = scroll_element['rect']['y']
|
1064
|
+
scroll_element_height = scroll_element['rect']['height']
|
899
1065
|
|
900
|
-
|
901
|
-
|
902
|
-
|
1066
|
+
if element_bottom > scroll_element_y + scroll_element_height
|
1067
|
+
scroll_by_y = element_bottom - (scroll_element_y + scroll_element_height)
|
1068
|
+
elsif element_y < scroll_element_y
|
1069
|
+
scroll_by_y = element_y - scroll_element_y
|
1070
|
+
else
|
1071
|
+
scroll_by_y = 0
|
1072
|
+
end
|
903
1073
|
|
904
|
-
|
905
|
-
|
906
|
-
|
1074
|
+
if scroll_by_y != 0
|
1075
|
+
result = query(scroll_view_query_string, {scrollBy: [0, scroll_by_y]}).first
|
1076
|
+
raise 'Could not scroll parent view' if result != '<VOID>'
|
1077
|
+
end
|
907
1078
|
|
908
|
-
|
909
|
-
|
910
|
-
|
1079
|
+
visibility_query_string = all_query_string[4..-1]
|
1080
|
+
when_element_exists(visibility_query_string, options)
|
1081
|
+
end
|
911
1082
|
|
912
|
-
|
913
|
-
|
914
|
-
|
1083
|
+
def scroll_to_row(uiquery,number)
|
1084
|
+
query(uiquery, {:smoothScrollToPosition => number})
|
1085
|
+
puts "TODO:detect end of scroll - use sleep for now"
|
1086
|
+
end
|
915
1087
|
|
916
|
-
|
917
|
-
|
918
|
-
screenshot_and_raise "No element found for query: #{query}"
|
1088
|
+
def rotate(dir)
|
1089
|
+
ni
|
919
1090
|
end
|
920
|
-
end
|
921
1091
|
|
922
|
-
|
923
|
-
|
924
|
-
screenshot_and_raise "Expected no elements to match query: #{query}"
|
1092
|
+
def app_to_background(secs)
|
1093
|
+
ni
|
925
1094
|
end
|
926
|
-
end
|
927
1095
|
|
928
|
-
|
929
|
-
|
930
|
-
|
1096
|
+
def element_does_not_exist(uiquery)
|
1097
|
+
query(uiquery).empty?
|
1098
|
+
end
|
931
1099
|
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
end
|
1100
|
+
def element_exists(uiquery)
|
1101
|
+
not element_does_not_exist(uiquery)
|
1102
|
+
end
|
936
1103
|
|
1104
|
+
def view_with_mark_exists(expected_mark)
|
1105
|
+
element_exists( "android.view.View marked:'#{expected_mark}'" )
|
1106
|
+
end
|
937
1107
|
|
938
|
-
|
939
|
-
|
940
|
-
|
1108
|
+
def check_element_exists( query )
|
1109
|
+
if not element_exists( query )
|
1110
|
+
screenshot_and_raise "No element found for query: #{query}"
|
1111
|
+
end
|
1112
|
+
end
|
941
1113
|
|
942
|
-
|
943
|
-
|
944
|
-
|
1114
|
+
def check_element_does_not_exist( query )
|
1115
|
+
if element_exists( query )
|
1116
|
+
screenshot_and_raise "Expected no elements to match query: #{query}"
|
1117
|
+
end
|
1118
|
+
end
|
945
1119
|
|
946
|
-
|
947
|
-
|
948
|
-
|
1120
|
+
def check_view_with_mark_exists(expected_mark)
|
1121
|
+
check_element_exists( "view marked:'#{expected_mark}'" )
|
1122
|
+
end
|
949
1123
|
|
950
|
-
|
951
|
-
|
952
|
-
|
1124
|
+
# a better name would be element_exists_and_is_not_hidden
|
1125
|
+
def element_is_not_hidden(uiquery)
|
1126
|
+
ni
|
1127
|
+
end
|
953
1128
|
|
954
|
-
def record_end(file_name)
|
955
|
-
ni
|
956
|
-
end
|
957
1129
|
|
958
|
-
|
959
|
-
|
960
|
-
if !result["success"]
|
961
|
-
screenshot_and_raise(result["message"])
|
1130
|
+
def load_playback_data(recording,options={})
|
1131
|
+
ni
|
962
1132
|
end
|
963
1133
|
|
964
|
-
|
965
|
-
|
966
|
-
|
1134
|
+
def playback(recording, options={})
|
1135
|
+
ni
|
1136
|
+
end
|
967
1137
|
|
968
|
-
|
969
|
-
|
970
|
-
:method_name => method_name,
|
971
|
-
:arguments => method_args
|
972
|
-
}
|
973
|
-
res = http("/map",
|
974
|
-
{:query => query, :operation => operation_map})
|
975
|
-
res = JSON.parse(res)
|
976
|
-
if res['outcome'] != 'SUCCESS'
|
977
|
-
screenshot_and_raise "map #{query}, #{method_name} failed because: #{res['reason']}\n#{res['details']}"
|
1138
|
+
def interpolate(recording, options={})
|
1139
|
+
ni
|
978
1140
|
end
|
979
1141
|
|
980
|
-
|
981
|
-
|
1142
|
+
def record_begin
|
1143
|
+
ni
|
1144
|
+
end
|
982
1145
|
|
983
|
-
|
984
|
-
|
985
|
-
|
1146
|
+
def record_end(file_name)
|
1147
|
+
ni
|
1148
|
+
end
|
986
1149
|
|
987
|
-
|
988
|
-
|
1150
|
+
def backdoor(method_name, arguments = [], options={})
|
1151
|
+
arguments = [arguments] unless arguments.is_a?(Array)
|
1152
|
+
|
1153
|
+
result = JSON.parse(http('/backdoor', {method_name: method_name, arguments: arguments}))
|
1154
|
+
|
1155
|
+
if result['outcome'] != 'SUCCESS'
|
1156
|
+
raise result.to_s
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
result['result']
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
def map(query, method_name, *method_args)
|
1163
|
+
operation_map = {
|
1164
|
+
:method_name => method_name,
|
1165
|
+
:arguments => method_args
|
1166
|
+
}
|
1167
|
+
res = http("/map",
|
1168
|
+
{:query => query, :operation => operation_map})
|
1169
|
+
res = JSON.parse(res)
|
1170
|
+
if res['outcome'] != 'SUCCESS'
|
1171
|
+
screenshot_and_raise "map #{query}, #{method_name} failed because: #{res['reason']}\n#{res['details']}"
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
res['results']
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
def url_for( method )
|
1178
|
+
default_device.url_for(method)
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
def make_http_request(options)
|
1182
|
+
default_device.make_http_request(options)
|
1183
|
+
end
|
989
1184
|
end
|
990
|
-
end
|
991
1185
|
|
992
1186
|
|
993
|
-
end end
|
1187
|
+
end end
|