aaet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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