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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +145 -0
- data/LICENSE +201 -0
- data/README.md +177 -0
- data/Rakefile +6 -0
- data/aaet.gemspec +51 -0
- data/bin/aaet +143 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/generate_reports.rb +512 -0
- data/lib/aaet.rb +43 -0
- data/lib/aaet/android/android.rb +190 -0
- data/lib/aaet/android/parser.rb +76 -0
- data/lib/aaet/common/applitools.rb +41 -0
- data/lib/aaet/common/common_methods.rb +646 -0
- data/lib/aaet/common/locators.rb +180 -0
- data/lib/aaet/common/redis.rb +108 -0
- data/lib/aaet/ios/ios.rb +18 -0
- data/lib/aaet/ios/parser.rb +1 -0
- data/lib/version.rb +3 -0
- data/run.rb +294 -0
- data/setup/android_setup_methods.rb +161 -0
- data/setup/common_setup_methods.rb +209 -0
- data/setup/ios_setup_methods.rb +11 -0
- data/template_google_api_format.haml +48 -0
- metadata +354 -0
data/lib/aaet.rb
ADDED
@@ -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
|