aaet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|