aaet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require "version"
2
+ require 'appium_lib'
3
+ require 'awesome_print'
4
+ require 'colorize'
5
+ require 'faker'
6
+ require 'redic'
7
+ require 'nokogiri'
8
+ require 'pp'
9
+
10
+ require_relative 'aaet/common/common_methods'
11
+
12
+ include Faker
13
+
14
+ module Aaet
15
+ class Runner
16
+
17
+ attr_accessor :common
18
+
19
+ def initialize settings
20
+ self.common = Aaet::Common.new(settings)
21
+ end
22
+
23
+ def monitor_log_start
24
+ common.start_log
25
+ end
26
+
27
+ def crawler
28
+ common.crawler
29
+ end
30
+
31
+ def replay
32
+ common.replay
33
+ end
34
+
35
+ def monkey
36
+ common.monkey
37
+ end
38
+
39
+ def current_activity
40
+ common.get_activity
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,190 @@
1
+ #http://www.vogella.com/tutorials/AndroidCommandLine/article.html
2
+ require_relative '../common/locators'
3
+ require_relative '../common/redis'
4
+
5
+ module Aaet
6
+ class Android < Aaet::Locators
7
+
8
+ attr_accessor :uuid, :dir, :log, :exception_pattern, :redis
9
+
10
+ def initialize settings
11
+ generate_instance_variables nil, settings
12
+ self.uuid = device[:uuid]
13
+ self.dir = output_dir
14
+ self.log = "exception-#{process}.log"
15
+ self.exception_pattern = "FATAL EXCEPTION"
16
+ self.redis = Aaet::Redis.new process
17
+ remove_status_bar_and_notifications #show only the application in the window
18
+ end
19
+
20
+ def generate_instance_variables(parent, hash)
21
+ #turn options/settings nested hash into instance variables
22
+ hash.each do |key, value|
23
+ if value.is_a?(Hash)
24
+ generate_instance_variables(key, value)
25
+ self.class.send(:attr_accessor, key.to_sym)
26
+ self.instance_variable_set("@#{key}", value)
27
+ else
28
+ if parent.nil?
29
+ self.class.send(:attr_accessor, "#{key}".to_sym)
30
+ self.instance_variable_set("@#{key}", value)
31
+ else
32
+ self.class.send(:attr_accessor, "#{parent}_#{key}".to_sym)
33
+ self.instance_variable_set("@#{parent}_#{key}", value)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def dialog_button
40
+ ({id: "android:id/button1"})
41
+ end
42
+
43
+ def remove_status_bar_and_notifications
44
+ unless options_cloud
45
+ #https://android.gadgethacks.com/how-to/hide-navigation-status-bars-your-galaxy-s8-for-even-more-screen-real-estate-no-root-needed-0177297/
46
+ puts "\nRemoving Top Status Bar from Device...\n".yellow
47
+ %x(adb -s #{uuid} shell 'settings put global policy_control immersive.status=*')
48
+ %x(adb -s #{uuid} shell 'appops set android POST_NOTIFICATION ignore')
49
+ sleep 1
50
+ end
51
+ end
52
+
53
+ def reset_status_bar
54
+ %x(adb -s #{uuid} shell settings put global policy_control null*) rescue nil #put status bar back...
55
+ end
56
+
57
+ def back_button
58
+ puts "\nClicking Android Back Button!!!\n".red
59
+ Appium::Common.back
60
+ end
61
+
62
+ def close_keyboard
63
+ #http://appium.readthedocs.io/en/latest/en/commands/device/keys/hide-keyboard/
64
+ if keyboard_open?
65
+ puts "\nClosing keyboard!!!\n"
66
+ #back_button
67
+ driver.hide_keyboard rescue nil
68
+ sleep 1
69
+ if keyboard_open? #sometimes keyboard doesn't close so try again...
70
+ #back_button
71
+ driver.hide_keyboard rescue nil
72
+ end
73
+ end
74
+ end
75
+
76
+ def permission_dialog_displayed? act
77
+ android_activities = ['.permission.ui.GrantPermissionsActivity', 'com.android.internal.app.ChooserActivity']
78
+ android_activities.any? { |a| a.include? act } rescue false
79
+ end
80
+
81
+ def close_permissions_dialog act
82
+ if permission_dialog_displayed? act
83
+ if act == "com.android.internal.app.ChooserActivity"
84
+ back_button
85
+ else
86
+ puts "\nDetected Android Permissions/Chooser Dialog. Closing...\n".yellow
87
+ fe({id: 'com.android.packageinstaller:id/permission_deny_button'}).click
88
+ end
89
+ sleep 1
90
+ end
91
+ end
92
+
93
+ def is_textfield? locator
94
+ "android.widget.EditText" == locator
95
+ end
96
+
97
+ def keyboard_open?
98
+ #http://appium.readthedocs.io/en/latest/en/commands/device/keys/is-keyboard-shown/
99
+ driver.is_keyboard_shown
100
+ #%x(adb shell dumpsys input_method)[/mInputShown=\w+/i].split('=')[1] == 'true'
101
+ #for iOS
102
+ #driver.find_element(:xpath, '//UIAKeyboard').displayed?
103
+ end
104
+
105
+ #Force type keyboard when opened...
106
+ def adb_type string
107
+ %x(adb -s #{uuid} shell input text "#{string}\n")
108
+ end
109
+
110
+ def system_stats
111
+ unless options_cloud
112
+ x = %x(adb -s #{uuid} shell top -n 1 -d 1 | grep System).split(",")
113
+ user = x.find { |x| x.include? "User" }.match(/User (.*)%/)[1].to_i rescue 0
114
+ sys = x.find { |x| x.include? "System" }.match(/System (.*)%/)[1].to_i rescue 0
115
+ # iow = x.find { |x| x.include? "IOW" }.match(/IOW (.*)%/)[1].to_i
116
+ # irq = x.find { |x| x.include? "IRQ" }.match(/IRQ (.*)%/)[1].to_i
117
+ { app_mem: memory, app_cpu: cpu, user: user, sys: sys }
118
+ end
119
+ end
120
+
121
+ def memory
122
+ memory = (%x(adb -s #{uuid} shell dumpsys meminfo | grep #{caps_appPackage} | awk '{print $1}').strip.split.last.to_i * 0.001).round(2)
123
+ puts "Memory: #{memory} MB"
124
+ memory
125
+ end
126
+
127
+ def cpu
128
+ cpu = %x(adb -s #{uuid} shell top -n 1 -d 1 | grep #{caps_appPackage} | awk '{print $3}').strip.chomp("%").to_i
129
+ puts "Cpu: #{cpu}%"
130
+ cpu
131
+ end
132
+
133
+ def logcat
134
+ %x(adb -s #{uuid} logcat -c)
135
+ @logcat_pid = spawn("adb -s #{uuid} logcat *:E -v long", :out=>"#{dir}/#{log}")
136
+ end
137
+
138
+ def process_running? pid
139
+ begin
140
+ Process.getpgid(pid)
141
+ true
142
+ rescue Errno::ESRCH
143
+ false
144
+ end
145
+ end
146
+
147
+ def kill_process process_name
148
+ `ps -ef | grep #{process_name} | awk '{print $2}' | xargs kill >> /dev/null 2>&1`
149
+ end
150
+
151
+ def kill_emulator emulator
152
+ system("adb -s #{emulator} emu kill")
153
+ end
154
+
155
+ def kill_everything
156
+ pid = Process.getpgid(Process.pid)
157
+ Signal.trap('TERM') { Process.kill('TERM', -pid); exit }
158
+ Signal.trap('INT' ) { Process.kill('INT', -pid); exit }
159
+ reset_status_bar
160
+ sleep 1
161
+ Process.kill("HUP", appium_pid)
162
+ Process.kill("SIGKILL", @logcat_pid)
163
+ #kill_process "#{uuid} logcat -v threadtime"
164
+ #kill_emulator uuid if options_emulator and options_kill_emulator
165
+ end
166
+
167
+ def start_log
168
+ unless options_cloud
169
+ logcat
170
+ Process.fork do
171
+ f = File.open("#{dir}/#{log}", "r")
172
+ f.seek(0,IO::SEEK_END)
173
+ while true
174
+ break unless process_running? @logcat_pid
175
+ select([f])
176
+ if f.gets =~ /#{exception_pattern}/
177
+ redis.app_crashed true
178
+ puts "\n******************* #{exception_pattern} DETECTED *******************\nSHUTTING DOWN...\n".red
179
+ sleep 3
180
+ break
181
+ end
182
+ end
183
+ f.close
184
+ #binding.pry
185
+ kill_everything
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,76 @@
1
+ module Aaet
2
+ class AndroidParser
3
+
4
+ class Appium::Android::AndroidElements
5
+
6
+ def reset
7
+ @result = []
8
+ end
9
+
10
+ def start_element(name, attrs = [], driver = driver)
11
+ return if filter && !name.downcase.include?(filter)
12
+
13
+ attributes = {}
14
+
15
+ do_not_include = ["android:id/content", "android:id/navigationBarBackground", "android:id/content",
16
+ "android:id/parentPanel", "android:id/topPanel", "android:id/title_template",
17
+ "android:id/contentPanel", "android:id/scrollView", "android:id/buttonPanel"]
18
+
19
+ attrs.each do |key, value|
20
+
21
+ #do not include this values
22
+ next if do_not_include.include? value
23
+
24
+ if key.include? "-"
25
+ key = key.gsub("-","_")
26
+ end
27
+
28
+ if key == "resource_id"
29
+ key = "id"
30
+ elsif key == "content_desc"
31
+ key = "accessibilty_label"
32
+ end
33
+
34
+ if ["android:id/button2", "android:id/button1"].include? value
35
+ attributes["dialog"] = true
36
+ end
37
+
38
+ if value.empty?
39
+ value = nil
40
+ end
41
+
42
+ if key == "bounds"
43
+ bounds_array = value.scan(/\d*/).reject { |c| c.empty? }.map { |v| v = v.to_i }
44
+ bounds_array_value = bounds_array.each_slice((bounds_array.size/2.0).round).to_a
45
+ attributes["bounds_array"] = bounds_array_value
46
+ end
47
+
48
+ attributes[key] = value
49
+ end
50
+
51
+ eval_attrs = ["checkable", "checked", "clickable", "enabled", "focusable", "focused",
52
+ "scrollable", "long_clickable", "password", "selected", "instance", "index"]
53
+
54
+ @result << attributes.reduce({}) do |memo, (k, v)|
55
+ if eval_attrs.include? k.to_s
56
+ v = eval(v) rescue false
57
+ end
58
+ memo.merge({ k.to_sym => v})
59
+ end
60
+ end
61
+ end
62
+
63
+ def page(opts = {})
64
+ class_name = opts.is_a?(Hash) ? opts.fetch(:class, nil) : opts
65
+ results = get_android_inspect class_name
66
+ results.map { |h| results.delete(h) if h.values.uniq == [nil] }
67
+ results
68
+ end
69
+
70
+ def print_page
71
+ page.each do |result|
72
+ puts "\n#{result}\n"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,41 @@
1
+ require 'eyes_selenium'
2
+
3
+ module Aaet
4
+ class ApplitoolEyes
5
+
6
+ attr_accessor :eyes, :applitools_settings, :uuid
7
+
8
+ def initialize settings
9
+ puts "\nRun Applitools Tests: true\n".green
10
+ self.eyes = Applitools::Selenium::Eyes.new
11
+ self.applitools_settings = settings[:config][:applitools][0]
12
+ eyes.api_key = applitools_settings[:key]
13
+ eyes.save_failed_tests = settings[:options][:updateBaseline]
14
+ batch_info = Applitools::BatchInfo.new(caps[:appPackage]) #app name, locale, orientation
15
+ batch_info.id = Digest::MD5.hexdigest(settings[:run_time]).scan(/\d/).join('')
16
+ eyes.batch = batch_info
17
+ eyes.match_level = :strict
18
+ self.uuid = settings[:config][:caps][:udid]
19
+ end
20
+
21
+ def eyes_open app_name, test_name
22
+ eyes.open(driver: driver, app_name: app_name, test_name: test_name)
23
+ end
24
+
25
+ def upload_to_applitools app_name, test_name, tag
26
+ eyes_open app_name, test_name
27
+ eyes.check_window tag
28
+ end
29
+
30
+ def close_eyes
31
+ results = eyes.close(false)
32
+ eyes.abort_if_not_closed
33
+ results
34
+ end
35
+
36
+ def tests
37
+ applitools_settings.delete(:key)
38
+ applitools_settings.map { |test| { name: test[0].to_s }.merge!(test[1]) }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,646 @@
1
+ require_relative 'redis'
2
+ require_relative 'applitools'
3
+ require_relative '../common/locators'
4
+ require_relative '../android/parser'
5
+ require_relative '../android/android'
6
+ require_relative '../ios/ios'
7
+ require_relative '../ios/parser'
8
+
9
+ module Aaet
10
+ class Common < Aaet::Locators
11
+
12
+ attr_accessor :applitools, :tests, :count, :redis, :execute, :parser, :wait_for_element, :dir, :uuid
13
+
14
+ def initialize settings
15
+ generate_instance_variables nil, settings
16
+
17
+ #Applitools only runs on crawler mode...
18
+ if options[:mode] == "crawler"
19
+ if options_applitools
20
+ self.applitools = Aaet::ApplitoolEyes.new settings
21
+ self.tests = applitools.tests
22
+ print_debug "\nApplitools Tests:".green
23
+ ap tests if options_debug
24
+ print_debug "\n"
25
+ end
26
+ end
27
+
28
+ self.count = "%03d" % 1
29
+ self.redis = Aaet::Redis.new process
30
+
31
+ if caps_platform == "android"
32
+ self.execute = Aaet::Android.new settings
33
+ self.parser = Aaet::AndroidParser.new
34
+ elsif caps_platform == "ios"
35
+ self.execute = Aaet::Ios.new settings #placeholder for iOS
36
+ self.parser = Aaet::IosParser.new
37
+ end
38
+
39
+ self.wait_for_element = 10
40
+ self.dir = output_dir
41
+
42
+ if options_cloud
43
+ self.uuid = "#{cloud_service}-#{caps_deviceName.gsub(" ","_")}"
44
+ else
45
+ self.uuid = device[:uuid]
46
+ end
47
+
48
+ @window_size = driver.manage.window.size.to_a
49
+ end
50
+
51
+ def generate_instance_variables(parent, hash)
52
+ #turn options/settings nested hash into instance variables
53
+ hash.each do |key, value|
54
+ if value.is_a?(Hash)
55
+ generate_instance_variables(key, value)
56
+ self.class.send(:attr_accessor, key.to_sym)
57
+ self.instance_variable_set("@#{key}", value)
58
+ else
59
+ if parent.nil?
60
+ self.class.send(:attr_accessor, "#{key}".to_sym)
61
+ self.instance_variable_set("@#{key}", value)
62
+ else
63
+ self.class.send(:attr_accessor, "#{parent}_#{key}".to_sym)
64
+ self.instance_variable_set("@#{parent}_#{key}", value)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def print_debug string
71
+ puts string if options_debug
72
+ end
73
+
74
+ def login_page?
75
+ print_debug "#{uuid}: Checking if on Login page..."
76
+ login_hash = config[:loginPage] rescue {}
77
+ if login_hash.any?
78
+ login if (activity == login_hash[0][:activity])
79
+ end
80
+ end
81
+
82
+ def login
83
+ puts "#{uuid}: ON THE LOGIN PAGE. I WILL LOGIN NOW!!!"
84
+ config_loginPage[0][:steps].each do |step|
85
+ driver.wait(config_loginPage[0][:maxWaitBetweenSteps]) { fe({:"#{step[1]}"=>step[2]}) }
86
+ print_debug "action: #{step[0]}, locator: :#{step[1]}=>#{step[2]}, text: #{step[3]}"
87
+ self.send(step[0], fe({:"#{step[1]}"=>step[2]}), step[3])
88
+ execute.close_keyboard if execute.keyboard_open?
89
+ sleep 5
90
+ end
91
+ wait_for_home_screen
92
+ #set_screen_boudaries
93
+ end
94
+
95
+ def wait_for_home_screen
96
+ Appium::Common.wait_true(wait_for_element) { activity == config[:homeActivity][:activity] }
97
+ end
98
+
99
+ def store_clicked_element body
100
+ print_debug "\n#{uuid}: Storing clicked element: #{body}\n"
101
+ redis.update_list "clicked", body
102
+ end
103
+
104
+ def clicked_elements
105
+ redis.get_list "clicked"
106
+ end
107
+
108
+ def start_log
109
+ execute.start_log
110
+ end
111
+
112
+ def stop_log
113
+ execute.stop_log
114
+ end
115
+
116
+ def outside_screen_boundaries?(screen_size, location)
117
+ if location[0] < 0 or location[1] < 0
118
+ true
119
+ elsif location[0] > screen_size[0] or location[1] > screen_size[1]
120
+ true
121
+ else
122
+ false
123
+ end
124
+ end
125
+
126
+ def uploaded_to_applitools test
127
+ #redis.update_list "applitools", test
128
+ redis.update_applitools "applitools", test
129
+ end
130
+
131
+ def has_uploaded? test
132
+ #redis.list_includes_value "applitools", test
133
+ redis.list_includes_applitools_value "applitools", test
134
+ end
135
+
136
+ def applitools_results results, test
137
+ hash = { test: test, failed: results.failed?, passed: results.passed?, new: results.new?, url: results.url }
138
+ #redis.update_list "applitools_results", hash
139
+ redis.update_applitools "applitools_results", hash
140
+ end
141
+
142
+ def is_test?
143
+ print_debug "#{uuid}: Checking for Applitool test..."
144
+ do_not_upload = tests.find_all { |test| test[:name] == "do_not_upload" }[0].select { |loc| loc if loc != :name } rescue []
145
+ current_activity_tests = tests.find_all { |test| test[:activity] == activity } rescue []
146
+ if displayed?(do_not_upload) #skip if do_not_upload locator displayed...
147
+ false
148
+ else
149
+ if current_activity_tests.empty?
150
+ false
151
+ else
152
+ current_activity_tests.each do |test|
153
+ #store screenshot name in redis if pushed to applitools so not to get duplicates.
154
+ locator = Hash[*test.to_a[2]]
155
+ unless fe(locator).nil? or get_text(test[:text]).nil?
156
+ test_name = "#{test[:name]}-#{device_resolution}"
157
+ unless has_uploaded? test_name
158
+ print_debug "\n#{uuid}: Uploading test '#{test_name}' to Applitools!\n".yellow
159
+ applitools.upload_to_applitools caps_appPackage, test_name, test[:text]
160
+ results = applitools.close_eyes
161
+ applitools_results results, test_name
162
+ uploaded_to_applitools test_name
163
+ #TODO: create a method to shutdown after test count matches redis completed test count...
164
+ #redis.hincr("applicount", "count")
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def take_screenshot?
173
+ md5 = md5_page_source
174
+ unless screenshot_exists? md5
175
+ is_test? if options_applitools
176
+ print_debug "#{uuid}: Taking a screenshot..."
177
+ screenshot "#{dir}/#{count}_#{md5}.png"
178
+ count.next!
179
+ end
180
+ end
181
+
182
+ def screenshot_exists? md5
183
+ files = Dir.entries(dir).select { |x| x.include? ".png" } rescue []
184
+ files.any? { |x| x.include? md5 } unless files.empty?
185
+ end
186
+
187
+ def relaunch_app
188
+ print_debug "#{uuid}: Launching App!!!"
189
+ `curl -s -X POST #{caps_url}/session/#{driver.session_id}/appium/app/launch`
190
+ #@new_page = []
191
+ sleep 5
192
+ end
193
+
194
+ def relaunch_app?
195
+ print_debug "#{uuid}: Checking to Relaunch app..."
196
+ act = activity
197
+ unless redis.activities.any? { |a| a.include? act }
198
+ if execute.permission_dialog_displayed? act
199
+ execute.close_permissions_dialog act
200
+ sleep 2
201
+ else
202
+ execute.back_button
203
+ unless redis.activities.any? { |a| a.include? activity }
204
+ relaunch_app
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ def page_changed? element
211
+ changed = md5_page_source != element[:page]
212
+ print_debug "\n#{uuid} The page has changed!!! Restarting...\n".red if changed
213
+ changed
214
+ end
215
+
216
+ def md5_page_source
217
+ print_debug "MD5'ing the Page Source..."
218
+ #Can also maybe use get_page_class to distinguish between pages...
219
+ Digest::MD5.hexdigest(get_source)
220
+ end
221
+
222
+ def string
223
+ #TODO create a CLI option to change which kind of strings are passed
224
+ chars = Lorem.characters(rand(1..100))
225
+ sentence = Faker::Hipster.sentence(1)
226
+ words = (Faker::Hipster.words(rand(1..10))).shuffle.join(" ")
227
+ url = Faker::Internet.url
228
+ mac = Faker::Internet.mac_address
229
+ #[chars, words, url, mac].sample
230
+ #Right now just sending hipster text but maybe randomize this in the future...
231
+ sentence
232
+ end
233
+
234
+ def close_keyboard
235
+ execute.close_keyboard if execute.keyboard_open?
236
+ end
237
+
238
+ def type_if_keyboard_is_open
239
+ if execute.keyboard_open?
240
+ print_debug "#{uuid}: Keyboard is open. I will type now..."
241
+ type "#{string}\n"
242
+ close_keyboard
243
+ string
244
+ end
245
+ end
246
+
247
+ def dialog_displayed?
248
+ displayed? execute.dialog_button
249
+ end
250
+
251
+ def accept_dialog
252
+ #maybe override this with appium capability to auto accept dialogs. Undecieded about it, though...
253
+ click execute.dialog_button if dialog_displayed?
254
+ end
255
+
256
+ def reset_dialog_count
257
+ redis.hset("dialog","count", 0)
258
+ end
259
+
260
+ def increment_dialog_count
261
+ redis.hincr("dialog","count")
262
+ end
263
+
264
+ def dialog_count
265
+ redis.hget("dialog", "count").to_i
266
+ end
267
+
268
+ def remove_clicked_element
269
+ redis.lpop "clicked"
270
+ end
271
+
272
+ def click_dialog?
273
+ if dialog_displayed?
274
+ if dialog_count >= 3
275
+ print_debug "#{uuid}: Clicking OK on Dialog!!!"
276
+ accept_dialog
277
+ reset_dialog_count
278
+ else
279
+ print_debug "#{uuid}: Skipping Dialog click..."
280
+ increment_dialog_count
281
+ return
282
+ end
283
+ else
284
+ reset_dialog_count
285
+ end
286
+ end
287
+
288
+ def weighted_actions
289
+ #TODO: implement these later... [multi_touch, shake, forcepress, longpress]
290
+ actions = ["random_tap", "swipe_right", "swipe_left", "pull_to_refresh", "buttons", "back", "swipe_down", "swipe_up"]
291
+ weights = [45, 15, 15, 10, 5, 5, 3, 2]
292
+ ps = weights.map { |w| (Float w) / weights.reduce(:+) }
293
+ weighted_actions = actions.zip(ps).to_h
294
+ wrs = -> (freq) { freq.max_by { |_, weight| rand ** (1.0 / weight) }.first }
295
+ action = wrs[weighted_actions]
296
+ print_debug "#{uuid}: Performing Action: #{action}".yellow
297
+ action
298
+ end
299
+
300
+ def collect_chaos_performance body
301
+ redis.update_list "chaos", body
302
+ end
303
+
304
+ def monkey
305
+ login_page?
306
+ relaunch_app?
307
+ #binding.pry
308
+ action = weighted_actions
309
+ if action == "buttons"
310
+ b = buttons
311
+ unless b.empty?
312
+ print_debug "#{uuid}: Clicking random button...".yellow
313
+ begin b.sample.click rescue nil end
314
+ end
315
+ elsif action == "back"
316
+ execute.back_button unless activity == homeActivity[:activity]
317
+ else
318
+ self.send(action)
319
+ end
320
+ sleep 0.3
321
+ #TODO: collect_chaos_performance({time: Time.now, performance: Thread.new { execute.system_stats }.value})
322
+ end
323
+
324
+ #TODO: Return boolean if replay step is displayed...
325
+ #use this to debug replay values
326
+ def check_replay_element_values step
327
+ element_values = []
328
+ [:bounds, :id, :accessibilty_label].each do |key|
329
+ next if step[key].nil?
330
+ if [:id, :accessibilty_label].include? key
331
+ element = fe({id: step[key]})
332
+ else
333
+ element = fe({xpath: "//#{step[:class]}[@bounds='#{step[:bounds]}']"})
334
+ end
335
+ next if element.nil?
336
+ location = element.location
337
+ size = element.size
338
+ element_values << {
339
+ locator_used: key,
340
+ element: element,
341
+ location: location.to_h,
342
+ step_location: step[:location],
343
+ center: element_center(location, size),
344
+ step_center: step[:center],
345
+ step_id: step[:id],
346
+ step_accessibilty_label: step[:accessibilty_label],
347
+ step_bounds: step[:bounds]
348
+ }
349
+ end
350
+ element_values
351
+ end
352
+
353
+ def replay
354
+ last_run_steps.each do |step|
355
+ #take_screenshot #not yet implemented logic yet to store screenshots in new location.
356
+ relaunch_app?
357
+ login_page?
358
+
359
+ locator = step[:accessibilty_label] || step[:id]
360
+
361
+ if locator.nil?
362
+ wait(wait_for_element) { fe({xpath: "//#{step[:class]}[@bounds='#{step[:bounds]}']"}) } rescue nil #will wait if element exists...
363
+ element = fe({xpath: "//#{step[:class]}[@bounds='#{step[:bounds]}']"})
364
+ else
365
+ wait(wait_for_element) { fe({id: locator}) } rescue nil #will wait if element exists...
366
+ element = fe({id: locator})
367
+ end
368
+
369
+ print_debug "\n#{uuid}: Last Run Step: #{step}\n"
370
+ print_debug ""
371
+
372
+ #binding.pry
373
+ #check_replay_element_values step
374
+
375
+ if step[:id] == "force-tap-back"
376
+ execute.back_button
377
+ else
378
+ element.click
379
+ end
380
+
381
+ if execute.keyboard_open? and step[:typed]
382
+ print_debug "\n#{uuid}: Typying Last Run Text: #{step[:typed]}\n"
383
+ type "#{step[:typed]}\n"
384
+ close_keyboard
385
+ else
386
+ type_if_keyboard_is_open
387
+ end
388
+
389
+ sleep 0.5
390
+ #step[:performance] = Thread.new { execute.system_stats }.value #To compare peformance from last test run...
391
+ end
392
+ sleep 5 #wait for a crash to happen
393
+ end
394
+
395
+ def new_activity current_activity
396
+ if activity == current_activity
397
+ nil
398
+ else
399
+ activity
400
+ end
401
+ end
402
+
403
+ def update_element_list act, elements
404
+ if redis.activity_exists? act
405
+ old_elements = redis.get_list(act)
406
+ new_elements = elements
407
+ diff = diff_actvity_elements(old_elements, new_elements) rescue []
408
+ unless diff.empty?
409
+ redis.del_list activity
410
+ select_elements = new_elements.select { |e| diff.map { |x| x[:id] }.include? e[:id] }
411
+ redis.update_list activity, (old_elements + select_elements)
412
+ end
413
+ redis.update_activity_count act
414
+ end
415
+ end
416
+
417
+ def diff_actvity_elements old_page, new_page
418
+ a = old_page.map { |x| { id: x[:id], label: x[:accessibilty_label] } }.uniq
419
+ b = new_page.map { |x| { id: x[:id], label: x[:accessibilty_label] } }.uniq
420
+ ( b - a )
421
+ end
422
+
423
+ def dont_click
424
+ config_doNotClick.map { |h| h.map { |k,v| v.values } }.flatten rescue []
425
+ end
426
+
427
+ def store_page_text body
428
+ print_debug "\n#{uuid}: Storing Page Text: #{body}\n"
429
+ redis.update_list "page_text", body
430
+ end
431
+
432
+ def store_accessibility_labels body
433
+ print_debug "\n#{uuid}: Storing Accessibility Labels: #{body}\n"
434
+ redis.update_list "accessibility_labels", body
435
+ end
436
+
437
+ def fix_orientation rotation
438
+ #TODO: Since parser.page tells the orientation we can do something with it....
439
+ if config[:caps][:caps][:orientation] == "PORTRAIT"
440
+ orientation = 0
441
+ else
442
+ orientation = 1
443
+ end
444
+ set_orientation = config[:caps][:caps][:orientation].downcase.to_sym
445
+ driver.rotate(set_orientation) if orientation != rotation
446
+ end
447
+
448
+ def get_elements page_objects = parser.page, act
449
+ print_debug "\n#{uuid}: Getting page elements..."
450
+ md5 = md5_page_source
451
+ a = act
452
+ elements = []
453
+ dialog = dialog_displayed?
454
+ rotation = page_objects[0][:rotation].to_i
455
+ page_objects.each do |o|
456
+ next unless (o[:enabled] and o[:clickable])
457
+ o[:dialog] = false if o[:dialog].nil?
458
+ elements << { activity: a, page: md5, dialog_displayed: dialog, rotation: rotation }.merge!(o)
459
+ end
460
+ elements = elements.uniq
461
+ update_element_list a, elements
462
+ page_text = page_objects.map { |t| t[:text] }.compact.reject { |e| e.empty? }
463
+ store_page_text([{activity: a, page: md5, text: page_text}])
464
+ accessibility_labels = page_objects.map { |l| l[:accessibilty_label] }.compact.reject { |e| e.empty? }
465
+ store_accessibility_labels([{activity: a, page: md5, text: accessibility_labels}])
466
+
467
+ objects = []
468
+ clicked_list = clicked_elements
469
+ elements.each { |e| been_clicked?(clicked_list, e); objects << e.merge!({click_count: @click_count, clicked_before: @has_clicked}) }
470
+ objects.shuffle.sort_by { |x| x[:click_count] }
471
+ end
472
+
473
+ def get_element_attributes object
474
+ locator = object[:accessibilty_label] || object[:id] #use accessibility label first and then id if available
475
+ if locator.nil?
476
+ element = fe({xpath: "//#{object[:class]}[@bounds='#{object[:bounds]}']"})
477
+ else
478
+ element = fe({id: locator})
479
+ end
480
+ return if element.nil?
481
+
482
+ location = element.location rescue nil
483
+ return if location.nil? or outside_screen_boundaries?(@window_size.to_a, location.to_a)
484
+ displayed = element.displayed? rescue false
485
+ return unless displayed
486
+ size = element.size
487
+
488
+ {
489
+ location: location.to_h,
490
+ displayed: displayed,
491
+ window_size: @window_size,
492
+ center: element_center(location, size),
493
+ element: element,
494
+ size: size.to_h
495
+ }.merge!(object)
496
+ end
497
+
498
+ def back_locator_displayed?
499
+ locators = config_backLocators.map { |h| h.map { |k,v| v } }.flatten rescue []
500
+ false if locators.empty?
501
+ locators.shuffle.each do |locator|
502
+ if displayed? locator
503
+ print_debug "\n#{uuid}: Tapping Back Locator: #{locator}\n".yellow
504
+ @back_locator = locator
505
+ return true
506
+ else
507
+ return false
508
+ end
509
+ end
510
+ end
511
+
512
+ def clicked_before?(e)
513
+ #center may change if scrolling is enabled. cant use accessibility label because that can change...
514
+ clicked = clicked_elements.find { |c|
515
+ c[:id] == e[:id] and c[:size] == e[:size] and c[:center] == e[:center] and c[:activity] == e[:activity] and c[:class] == e[:class]
516
+ }
517
+ @click_count = clicked[:click_count] rescue 0
518
+ clicked.any? rescue false
519
+ end
520
+
521
+ def been_clicked?(clicked_list, e)
522
+ #add a weighted selection on how many clicks have occured...
523
+ clicked = clicked_list.find do |x|
524
+ x[:index] == e[:index] and
525
+ x[:class] == e[:class] and
526
+ x[:package] == e[:package] and
527
+ x[:checkable] == e[:checkable] and
528
+ #x[:checked] == e[:checked] and
529
+ x[:clickable] == e[:clickable] and
530
+ x[:focusable] == e[:focusable] and
531
+ x[:focused] == e[:focused] and
532
+ x[:scrollable] == e[:scrollable] and
533
+ x[:long_clickable] == e[:long_clickable] and
534
+ #x[:selected] == e[:selected] and
535
+ x[:bounds] == e[:bounds] and
536
+ x[:id] == e[:id] and
537
+ x[:instance] == e[:instance] and
538
+ x[:clickable] == e[:clickable] and
539
+ x[:enabled] == e[:enabled] and
540
+ x[:activity] == e[:activity]
541
+ end
542
+ @click_count = clicked[:click_count] rescue 0
543
+ begin
544
+ clicked.any?
545
+ @has_clicked = true
546
+ rescue
547
+ @has_clicked = false
548
+ false
549
+ end
550
+ end
551
+
552
+ def crawler
553
+ print_debug "\n#{uuid}: Starting!!!\n".green
554
+
555
+ take_screenshot?
556
+ relaunch_app?
557
+ login_page?
558
+
559
+ current_activity = activity
560
+ print_debug "#{uuid}: Current Activity: #{current_activity}"
561
+
562
+ #binding.pry
563
+
564
+ objects = get_elements current_activity
565
+ if objects.empty?
566
+ if current_activity != homeActivity[:activity]
567
+ execute.back_button
568
+ else
569
+ relaunch_app
570
+ end
571
+ return
572
+ end
573
+
574
+ catch(:stop) do
575
+ print_debug "#{uuid}: Objects Count: #{objects.count}\n"
576
+ objects.each_with_index do |o,oi|
577
+ print_debug "\n#{uuid}: INDEX: #{oi}\nOBJECT: #{o}\n"
578
+
579
+ e = get_element_attributes(o)
580
+ next if e.nil? or dont_click.include? e[:id]
581
+
582
+ print_debug "\n#{uuid}: Using Element: #{e}\n"
583
+ e[:page_changed] = false
584
+
585
+ if e[:click_count] >= settings[:click_count]
586
+ if oi == objects.size - 1
587
+ if back_locator_displayed?
588
+ e = get_element_attributes(@back_locator) #reset element to config_backLocators attributes...
589
+ been_clicked?(clicked_elements, e)
590
+ e.merge!({click_count: @click_count, activity: o[:activity], page: o[:page]}) #merge object attributes into e.
591
+ else
592
+ #this logic will need to change for iOS unless we can create a method to simulate a back button like android has...
593
+ e = { click_count: 0, class: nil, text: nil, location: nil, center: nil, element: nil, id: "force-tap-back" }
594
+ e.merge!({activity: o[:activity], page: o[:page]}) #merge object attributes into e.
595
+ end
596
+ else
597
+ print_debug "\n#{uuid}: Skipping Element: #{e}".yellow
598
+ print_debug "#{uuid}: I've tapped this Locator #{@click_count} times before...\n".yellow
599
+ next
600
+ end
601
+ end
602
+
603
+ e[:clicked] = true #store clicked element in case app crashes when clicked.
604
+
605
+ #TODO: Set rules hash on click counts by locator class. e.g. textfield, button etc...
606
+ if e[:class] == "android.widget.EditText" #only click textfields once
607
+ e[:click_count] = 3
608
+ else
609
+ e[:click_count] = e[:click_count] + 1
610
+ end
611
+
612
+ if e[:dialog]
613
+ e[:click_count] = 0
614
+ end
615
+
616
+ e[:time] = Time.now
617
+ store_clicked_element e
618
+
619
+ if e[:id] == "force-tap-back"
620
+ execute.back_button; sleep 0.2
621
+ if e[:page] == md5_page_source
622
+ #at a last resort relaunch the app...
623
+ print_debug "\n#{uuid}: Stuck on the same view/page. Getting outta here...\n".red
624
+ relaunch_app
625
+ end
626
+ else
627
+ click e[:element]
628
+ end
629
+
630
+ sleep 0.2
631
+ e[:typed] = type_if_keyboard_is_open
632
+
633
+ if page_changed? e
634
+ remove_clicked_element
635
+ e[:page_changed] = true
636
+ e[:performance] = Thread.new { execute.system_stats }.value
637
+ e[:new_activity] = new_activity(e[:activity])
638
+ e[:new_page] = md5_page_source
639
+ store_clicked_element e
640
+ throw :stop
641
+ end
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end