illuminator 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/gem/README.md +37 -0
- data/gem/bin/illuminatorTestRunner.rb +22 -0
- data/gem/lib/illuminator.rb +171 -0
- data/gem/lib/illuminator/argument-parsing.rb +299 -0
- data/gem/lib/illuminator/automation-builder.rb +39 -0
- data/gem/lib/illuminator/automation-runner.rb +589 -0
- data/gem/lib/illuminator/build-artifacts.rb +118 -0
- data/gem/lib/illuminator/device-installer.rb +45 -0
- data/gem/lib/illuminator/host-utils.rb +42 -0
- data/gem/lib/illuminator/instruments-runner.rb +301 -0
- data/gem/lib/illuminator/javascript-runner.rb +98 -0
- data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
- data/gem/lib/illuminator/listeners/full-output.rb +13 -0
- data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
- data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
- data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
- data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
- data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
- data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
- data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
- data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
- data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
- data/gem/lib/illuminator/options.rb +96 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
- data/gem/lib/illuminator/test-definitions.rb +23 -0
- data/gem/lib/illuminator/test-suite.rb +155 -0
- data/gem/lib/illuminator/version.rb +3 -0
- data/gem/lib/illuminator/xcode-builder.rb +144 -0
- data/gem/lib/illuminator/xcode-utils.rb +219 -0
- data/gem/resources/BuildConfiguration.xcconfig +10 -0
- data/gem/resources/js/AppMap.js +767 -0
- data/gem/resources/js/Automator.js +1132 -0
- data/gem/resources/js/Base64.js +142 -0
- data/gem/resources/js/Bridge.js +102 -0
- data/gem/resources/js/Config.js +92 -0
- data/gem/resources/js/Extensions.js +2025 -0
- data/gem/resources/js/Illuminator.js +228 -0
- data/gem/resources/js/Preferences.js +24 -0
- data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
- data/gem/resources/scripts/common.applescript +25 -0
- data/gem/resources/scripts/diff_png.sh +61 -0
- data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
- data/gem/resources/scripts/plist_to_json.sh +40 -0
- data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
- metadata +225 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Illuminator
|
5
|
+
|
6
|
+
# Convenience functions for command-line actions done in Xcode
|
7
|
+
class XcodeUtils
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@xcode_path = `/usr/bin/xcode-select -print-path`.chomp.sub(/^\s+/, '')
|
12
|
+
@xcode_version = nil
|
13
|
+
@sdk_path = nil
|
14
|
+
@instruments_path = nil
|
15
|
+
@_simulator_devices_text = nil
|
16
|
+
@simulator_device_types = nil
|
17
|
+
@simulator_runtimes = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_xcode_path
|
21
|
+
@xcode_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_xcode_app_path
|
25
|
+
Illuminator::HostUtils.realpath(File.join(@xcode_path, "../../"))
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_xcode_simctl_path
|
29
|
+
Illuminator::HostUtils.realpath(File.join(@xcode_path, "/usr/bin/simctl"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_xcode_version
|
33
|
+
if @xcode_version.nil?
|
34
|
+
xcode_version = `xcodebuild -version`
|
35
|
+
needle = 'Xcode (.*)'
|
36
|
+
match = xcode_version.match(needle)
|
37
|
+
@xcode_version = match.captures[0]
|
38
|
+
end
|
39
|
+
@xcode_version
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_xcode_major_version ver
|
43
|
+
# should update this with 'version' gem
|
44
|
+
needle = '(\d+)\.?(\d+)?'
|
45
|
+
match = get_xcode_version.match(needle)
|
46
|
+
return match.captures[0].to_i == ver
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the path to the SDK
|
50
|
+
def get_sdk_path
|
51
|
+
@sdk_path ||= `/usr/bin/xcodebuild -version -sdk iphoneos | grep PlatformPath`.split(':')[1].chomp.sub(/^\s+/, '')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the path to the instruments bundle
|
55
|
+
def get_instruments_path
|
56
|
+
if @instruments_path.nil?
|
57
|
+
if File.directory? "#{@xcode_path}/../Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.xrplugin/"
|
58
|
+
@instruments_path = "AutomationInstrument.xrplugin";
|
59
|
+
else
|
60
|
+
#fallback to old instruments bundle (pre Xcode6)
|
61
|
+
@instruments_path = "AutomationInstrument.bundle";
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@instruments_path
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the path to the instruments template
|
68
|
+
def get_instruments_template_path
|
69
|
+
File.join(@xcode_path,
|
70
|
+
"../Applications/Instruments.app/Contents/PlugIns",
|
71
|
+
get_instruments_path,
|
72
|
+
"Contents/Resources/Automation.tracetemplate")
|
73
|
+
end
|
74
|
+
|
75
|
+
def _get_all_simulator_info
|
76
|
+
info = `#{get_xcode_simctl_path} list`.split("\n")
|
77
|
+
|
78
|
+
output = {"devices" => [], "runtimes" => []}
|
79
|
+
pointer = nil
|
80
|
+
needle = nil
|
81
|
+
|
82
|
+
info.each do |line|
|
83
|
+
case line
|
84
|
+
when "== Device Types =="
|
85
|
+
# all data we want is followed by " (" e.g. "iPhone 5 (com.apple.C......."
|
86
|
+
pointer = "devices"
|
87
|
+
needle = '([^(]*) '
|
88
|
+
when "== Runtimes =="
|
89
|
+
# all data we want is in the form "iOS 7.0 (7.0.3 - ........." and we want the 7.0
|
90
|
+
pointer = "runtimes"
|
91
|
+
needle = 'iOS ([^(]*) '
|
92
|
+
when "== Devices =="
|
93
|
+
pointer = nil
|
94
|
+
needle = nil
|
95
|
+
else
|
96
|
+
unless pointer.nil?
|
97
|
+
match = line.match(needle)
|
98
|
+
if match
|
99
|
+
output[pointer] << match.captures[0]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
@simulator_device_types = output["devices"]
|
106
|
+
@simulator_runtimes = output["runtimes"]
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_simulator_device_types
|
110
|
+
if @simulator_device_types.nil?
|
111
|
+
_get_all_simulator_info
|
112
|
+
end
|
113
|
+
@simulator_device_types
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_simulator_runtimes
|
117
|
+
if @simulator_runtimes.nil?
|
118
|
+
_get_all_simulator_info
|
119
|
+
end
|
120
|
+
@simulator_runtimes
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_simulator_devices
|
124
|
+
if @_simulator_devices_text.nil?
|
125
|
+
@_simulator_devices_text = `instruments -s devices`
|
126
|
+
end
|
127
|
+
@_simulator_devices_text
|
128
|
+
end
|
129
|
+
|
130
|
+
# Based on the desired device and version, get the ID of the simulator that will be passed to instruments
|
131
|
+
def get_simulator_id (sim_device, sim_version)
|
132
|
+
devices = get_simulator_devices
|
133
|
+
needle = sim_device + ' \(' + sim_version + ' Simulator\) \[(.*)\]'
|
134
|
+
match = devices.match(needle)
|
135
|
+
if match
|
136
|
+
puts "Found device match: #{match}".green
|
137
|
+
return match.captures[0]
|
138
|
+
elsif
|
139
|
+
puts "Did not find UDID of device '#{sim_device}' for version '#{sim_version}'".green
|
140
|
+
if XcodeUtils.instance.is_xcode_major_version 5
|
141
|
+
fallback_name = "#{sim_device} - Simulator - iOS #{sim_version}"
|
142
|
+
puts "Falling back to Xcode5 name #{fallback_name}".green
|
143
|
+
return fallback_name
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
return nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_crash_directory
|
151
|
+
return "#{ENV['HOME']}/Library/Logs/DiagnosticReports"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create a crash report
|
155
|
+
def create_symbolicated_crash_report (app_path, crash_path, crash_report_path)
|
156
|
+
# find symbolicatecrash file, which is different depending on the Xcode version (we assume either 5 or 6)
|
157
|
+
frameworks_path = "#{@xcode_path}/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks"
|
158
|
+
symbolicator_path = "#{frameworks_path}/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash"
|
159
|
+
if not File.exist?(symbolicator_path)
|
160
|
+
symbolicator_path = "#{frameworks_path}/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash"
|
161
|
+
end
|
162
|
+
if not File.exist?(symbolicator_path)
|
163
|
+
symbolicator_path = File.join(get_xcode_app_path,
|
164
|
+
"Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash")
|
165
|
+
end
|
166
|
+
|
167
|
+
command = "DEVELOPER_DIR='#{@xcode_path}' "
|
168
|
+
command << "'#{symbolicator_path}' "
|
169
|
+
command << "-o '#{crash_report_path}' '#{crash_path}' '#{app_path}.dSYM' 2>&1"
|
170
|
+
|
171
|
+
output = `#{command}`
|
172
|
+
|
173
|
+
# log the output of the crash reporting if the file didn't appear
|
174
|
+
unless File.exist?(crash_report_path)
|
175
|
+
puts command.green
|
176
|
+
puts output
|
177
|
+
return false
|
178
|
+
end
|
179
|
+
return true
|
180
|
+
end
|
181
|
+
|
182
|
+
# use the provided applescript to reset the content and settings of the simulator
|
183
|
+
def reset_simulator device_id
|
184
|
+
command = "#{get_xcode_simctl_path} erase #{device_id}"
|
185
|
+
puts command.green
|
186
|
+
puts `#{command}`
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
def shutdown_simulator device_id
|
191
|
+
command = "#{get_xcode_simctl_path} shutdown #{device_id}"
|
192
|
+
puts command.green
|
193
|
+
puts `#{command}`
|
194
|
+
end
|
195
|
+
|
196
|
+
# remove any apps in the specified directory
|
197
|
+
def self.remove_existing_apps xcode_output_dir
|
198
|
+
Dir["#{xcode_output_dir}/*.app"].each do |app|
|
199
|
+
puts "XcodeUtils: removing #{app}"
|
200
|
+
FileUtils.rm_rf app
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.kill_all_simulator_processes(device_id = nil)
|
205
|
+
XcodeUtils.instance.shutdown_simulator(device_id) unless device_id.nil?
|
206
|
+
command = HostUtils.realpath(File.join(File.dirname(__FILE__), "../../resources/scripts/kill_all_sim_processes.sh"))
|
207
|
+
puts "Running #{command}"
|
208
|
+
puts `'#{command}'`
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.kill_all_instruments_processes
|
212
|
+
command = "killall -9 instruments"
|
213
|
+
puts "Running #{command}"
|
214
|
+
puts `#{command}`
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
@@ -0,0 +1,767 @@
|
|
1
|
+
// AppMap.js
|
2
|
+
//
|
3
|
+
// creates 'appmap' which can build and store app definitions
|
4
|
+
// apps have screens
|
5
|
+
// screens have actions
|
6
|
+
// actions have parameters
|
7
|
+
// actions have device-specific implementations
|
8
|
+
|
9
|
+
var debugAppmap = false;
|
10
|
+
|
11
|
+
(function() {
|
12
|
+
|
13
|
+
var root = this,
|
14
|
+
appmap = null;
|
15
|
+
|
16
|
+
// put appmap in namespace of importing code
|
17
|
+
if (typeof exports !== 'undefined') {
|
18
|
+
appmap = exports;
|
19
|
+
} else {
|
20
|
+
appmap = root.appmap = {};
|
21
|
+
}
|
22
|
+
|
23
|
+
// Exception classes
|
24
|
+
appmap.SetupException = makeErrorClassWithGlobalLocator(__file__(), "AppMapSetupException");
|
25
|
+
appmap.ParameterException = makeErrorClass("AppMapParameterException");
|
26
|
+
|
27
|
+
// internal variables
|
28
|
+
appmap.apps = {}; // all possible apps, keyed by string
|
29
|
+
var lastApp; // state variable for building app
|
30
|
+
var lastAppName; // name of last app
|
31
|
+
var lastScreen; // state variable for building screen
|
32
|
+
var lastScreenName; // name of last screen
|
33
|
+
var lastAction; // state variable for building action
|
34
|
+
var lastActionName; // name of last action
|
35
|
+
var lastScreenActiveFn; // action function map of last screen
|
36
|
+
|
37
|
+
appmap.inputMethods = {}; // all possible input methods, keyed by string
|
38
|
+
var lastInputMethod; // state variable for building input method
|
39
|
+
var lastInputMethodName; // name of last input method
|
40
|
+
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Create a new app in the appmap with the given name.
|
44
|
+
*
|
45
|
+
* All following screen defintions will be associated with this app.
|
46
|
+
*
|
47
|
+
* @param appName the desired app name
|
48
|
+
* @return this
|
49
|
+
*/
|
50
|
+
appmap.createApp = function(appName) {
|
51
|
+
appmap.lastApp = {}; // screens is empty
|
52
|
+
appmap.lastAppName = appName;
|
53
|
+
appmap.apps[appName] = appmap.lastApp;
|
54
|
+
appmap.lastScreen = null;
|
55
|
+
appmap.lastAction = null;
|
56
|
+
return this;
|
57
|
+
};
|
58
|
+
|
59
|
+
/**
|
60
|
+
* whether an app exists
|
61
|
+
*
|
62
|
+
* @param appName the app name to test
|
63
|
+
* @return bool
|
64
|
+
*/
|
65
|
+
appmap.hasApp = function(appName) {
|
66
|
+
return appName in appmap.apps;
|
67
|
+
};
|
68
|
+
|
69
|
+
/**
|
70
|
+
* All following screen defintions will be associated with this app.
|
71
|
+
*
|
72
|
+
* @param appName the desired app name
|
73
|
+
* @return this
|
74
|
+
*/
|
75
|
+
appmap.augmentApp = function(appName) {
|
76
|
+
appmap.lastApp = appmap.apps[appName];
|
77
|
+
appmap.lastAppName = appName;
|
78
|
+
return this;
|
79
|
+
};
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Create a new app if it does not already exist.
|
83
|
+
*
|
84
|
+
* All following screen definitions will be associated with this app.
|
85
|
+
*
|
86
|
+
* @param appName the desired app name
|
87
|
+
* @return this
|
88
|
+
*/
|
89
|
+
appmap.createOrAugmentApp = function(appName) {
|
90
|
+
return appmap.hasApp(appName) ? appmap.augmentApp(appName) : appmap.createApp(appName);
|
91
|
+
}
|
92
|
+
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Create a new screen in the appmap with the given name.
|
96
|
+
*
|
97
|
+
* All following target/action defintions will be associated with this screen.
|
98
|
+
*
|
99
|
+
* @param screenName the desired screen name
|
100
|
+
* @return this
|
101
|
+
*/
|
102
|
+
appmap.withNewScreen = function(screenName) {
|
103
|
+
appmap.lastScreen = {};
|
104
|
+
appmap.lastScreenName = screenName;
|
105
|
+
appmap.lastScreenActiveFn = {};
|
106
|
+
appmap.lastAction = null;
|
107
|
+
|
108
|
+
appmap.lastApp[appmap.lastScreenName] = appmap.lastScreen;
|
109
|
+
if (debugAppmap) UIALogger.logDebug(" adding screen " + appmap.lastScreenName);
|
110
|
+
|
111
|
+
return this;
|
112
|
+
};
|
113
|
+
|
114
|
+
/**
|
115
|
+
* whether a screen exists
|
116
|
+
*
|
117
|
+
* @param appName the app to look in
|
118
|
+
* @param screenName the screen name to test
|
119
|
+
* @return bool
|
120
|
+
*/
|
121
|
+
appmap.hasScreen = function(appName, screenName) {
|
122
|
+
return appmap.hasApp(appName) && (screenName in appmap.apps[appName]);
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* All following target / action defintions will be associated with this screen
|
127
|
+
*
|
128
|
+
* @param appName the desired app name
|
129
|
+
* @return this
|
130
|
+
*/
|
131
|
+
appmap.augmentScreen = function(screenName) {
|
132
|
+
appmap.lastScreenName = screenName;
|
133
|
+
|
134
|
+
appmap.lastScreen = appmap.lastApp[appmap.lastScreenName];
|
135
|
+
if (debugAppmap) UIALogger.logDebug(" augmenting screen " + appmap.lastScreenName);
|
136
|
+
return this;
|
137
|
+
}
|
138
|
+
|
139
|
+
/**
|
140
|
+
* Create a new screen if it does not already exist.
|
141
|
+
*
|
142
|
+
* All following target / action definitions will be associated with this screen.
|
143
|
+
*
|
144
|
+
* @param screenName the desired screen name
|
145
|
+
* @return this
|
146
|
+
*/
|
147
|
+
appmap.withScreen = function(screenName) {
|
148
|
+
return appmap.hasScreen(appmap.lastAppName, screenName) ? appmap.augmentScreen(screenName) : appmap.withNewScreen(screenName);
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Enable the screen on a given target device
|
153
|
+
*
|
154
|
+
* @param targetName the name of the target device (e.g. "iPhone", "iPad")
|
155
|
+
* @param isActiveFn function that should return true when the screen is currently both visible and accessible
|
156
|
+
* @return this
|
157
|
+
*/
|
158
|
+
appmap.onTarget = function(targetName, isActiveFn) {
|
159
|
+
var lastScreenName = appmap.lastScreenName;
|
160
|
+
if (debugAppmap) UIALogger.logDebug(" on Target " + targetName);
|
161
|
+
appmap.lastScreenActiveFn[targetName] = isActiveFn;
|
162
|
+
|
163
|
+
appmap.withAction("verifyIsActive", "Null op to verify that the " + appmap.lastScreenName + " screen is active");
|
164
|
+
// slightly hacky, withImplementation expects actions to come AFTER all the onTarget calls
|
165
|
+
appmap.lastAction.isCorrectScreen[targetName] = isActiveFn;
|
166
|
+
appmap.withImplementation(function() {}, targetName);
|
167
|
+
|
168
|
+
appmap.withAction("verifyNotActive", "Verify that the " + appmap.lastScreenName + " screen is NOT active")
|
169
|
+
// slightly hacky, withImplementation expects actions to come AFTER all the onTarget calls
|
170
|
+
appmap.lastAction.isCorrectScreen[targetName] = isActiveFn;
|
171
|
+
appmap.withImplementation(function() {
|
172
|
+
if (isActiveFn()) {
|
173
|
+
throw new IlluminatorRuntimeVerificationException("Failed assertion that '" + lastScreenName + "' is NOT active ");
|
174
|
+
}
|
175
|
+
}, targetName);
|
176
|
+
|
177
|
+
// now modify verifyNotActive's isCorrectScreen array to always return true. slighly hacky.
|
178
|
+
// this is because the meat of the function runs in our generated action
|
179
|
+
for (var d in appmap.lastAction.isCorrectScreen) {
|
180
|
+
appmap.lastAction.isCorrectScreen[d] = function (parm) {
|
181
|
+
delay((parm === undefined || parm.delay === undefined) ? 0.35 : parm.delay); // wait for animations to complete
|
182
|
+
UIALogger.logDebug("verifyNotActive is skipping the default screenIsActive function");
|
183
|
+
return true;
|
184
|
+
};
|
185
|
+
}
|
186
|
+
|
187
|
+
return this;
|
188
|
+
};
|
189
|
+
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Create a new action in the appmap with the given name.
|
193
|
+
*
|
194
|
+
* All following implementation / parameter definitions will be associated with this action.
|
195
|
+
*
|
196
|
+
* @param actionName the desired screen name
|
197
|
+
* @param desc the description of this action that will be used for logging purposes
|
198
|
+
* @return this
|
199
|
+
*/
|
200
|
+
appmap.withNewAction = function(actionName, desc) {
|
201
|
+
// we need this hack to prevent problems with the above isCorrectScreen hack --
|
202
|
+
// so that we don't change the original reference, we rebuild the {targetname : function} map manually
|
203
|
+
var frozen = function(k) { return appmap.lastScreenActiveFn[k]; };
|
204
|
+
var isActiveMap = {};
|
205
|
+
for (var k in appmap.lastScreenActiveFn) {
|
206
|
+
isActiveMap[k] = frozen(k);
|
207
|
+
}
|
208
|
+
|
209
|
+
// we add screen params to the action so that we can deal in actions alone
|
210
|
+
appmap.lastAction = {
|
211
|
+
name: actionName,
|
212
|
+
isCorrectScreen: isActiveMap,
|
213
|
+
appName: appmap.lastAppName,
|
214
|
+
screenName: appmap.lastScreenName,
|
215
|
+
actionFn: {},
|
216
|
+
description: desc,
|
217
|
+
params: {}
|
218
|
+
};
|
219
|
+
appmap.lastActionName = actionName;
|
220
|
+
if (debugAppmap) UIALogger.logDebug(" adding action " + appmap.lastActionName);
|
221
|
+
appmap.lastScreen[appmap.lastActionName] = appmap.lastAction;
|
222
|
+
return this;
|
223
|
+
};
|
224
|
+
|
225
|
+
|
226
|
+
/**
|
227
|
+
* whether an action exists
|
228
|
+
*
|
229
|
+
* @param appName the app to look in
|
230
|
+
* @param screenName tge screen to look in
|
231
|
+
* @param actionName the screen name to test
|
232
|
+
* @return bool
|
233
|
+
*/
|
234
|
+
appmap.hasAction = function(appName, screenName, actionName) {
|
235
|
+
return appmap.hasScreen(appName, screenName) && (actionName in appmap.apps[appName][screenName]);
|
236
|
+
}
|
237
|
+
|
238
|
+
|
239
|
+
/**
|
240
|
+
* All following implementation / parameter definitions will be associated with this screen
|
241
|
+
*
|
242
|
+
* @param appName the desired app name
|
243
|
+
* @return this
|
244
|
+
*/
|
245
|
+
appmap.augmentAction = function(actionName) {
|
246
|
+
if (debugAppmap) UIALogger.logDebug(" augmenting action " + actionName);
|
247
|
+
appmap.lastAction = appmap.lastScreen[actionName];
|
248
|
+
appmap.lastActionName = actionName;
|
249
|
+
return this;
|
250
|
+
}
|
251
|
+
|
252
|
+
/**
|
253
|
+
* Create a new action in the appmap with the given name if it does not already exist.
|
254
|
+
*
|
255
|
+
* All following implementation / parameter definitions will be associated with this action.
|
256
|
+
*
|
257
|
+
* @param actionName the desired screen name
|
258
|
+
* @param desc the description of this action that will be used for logging purposes
|
259
|
+
* @return this
|
260
|
+
*/
|
261
|
+
appmap.withAction = function(actionName, desc) {
|
262
|
+
return appmap.hasAction(appmap.lastAppName, appmap.lastScreenName, actionName) ? appmap.augmentAction(actionName) : appmap.withNewAction(actionName, desc);
|
263
|
+
}
|
264
|
+
|
265
|
+
/**
|
266
|
+
* Set the function that will carry out the current action on the given target device
|
267
|
+
*
|
268
|
+
* targetName is optional; if omitted, the function will be used for all target devices
|
269
|
+
*
|
270
|
+
* @param actFn function that should be executed when the current action is requested to run
|
271
|
+
* @param targetName optional the name of the target device (e.g. "iPhone", "iPad")
|
272
|
+
* @return this
|
273
|
+
*/
|
274
|
+
appmap.withImplementation = function(actFn, targetName) {
|
275
|
+
targetName = targetName === undefined ? "default" : targetName;
|
276
|
+
|
277
|
+
// catch implementations for nonexistent targets
|
278
|
+
if ("default" != targetName && !(targetName in appmap.lastAction.isCorrectScreen)) {
|
279
|
+
var targets = [];
|
280
|
+
for (var k in appmap.lastAction.isCorrectScreen) {
|
281
|
+
targets.push(k);
|
282
|
+
}
|
283
|
+
var msg = "Screen " + appmap.lastAppName + "." + appmap.lastScreenName;
|
284
|
+
msg += " only has defined targets: '" + targets.join("', '") + "' but tried to add an implementation";
|
285
|
+
msg += " for target device '" + targetName + "' in action '" + appmap.lastActionName + "'";
|
286
|
+
throw new appmap.SetupException(msg);
|
287
|
+
}
|
288
|
+
|
289
|
+
if (debugAppmap) UIALogger.logDebug(" adding implementation on " + targetName);
|
290
|
+
appmap.lastAction.actionFn[targetName] = actFn;
|
291
|
+
return this;
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Define a new parameter in the latest action with the given attributes
|
296
|
+
*
|
297
|
+
* @param paramName the name of the parameter
|
298
|
+
* @param desc a description of the parameter
|
299
|
+
* @param required bool whether the parameter is required for the action
|
300
|
+
* @param useInSummary whether the value of the parameter should be logged when the action executes
|
301
|
+
*/
|
302
|
+
appmap.withParam = function(paramName, desc, required, useInSummary) {
|
303
|
+
if (debugAppmap) UIALogger.logDebug(" adding parameter " + paramName);
|
304
|
+
useInSummmary = useInSummary === undefined ? false : useInSummary;
|
305
|
+
appmap.lastAction.params[paramName] = {
|
306
|
+
description: desc,
|
307
|
+
required: required,
|
308
|
+
useInSummary: useInSummary
|
309
|
+
};
|
310
|
+
return this;
|
311
|
+
};
|
312
|
+
|
313
|
+
|
314
|
+
/**
|
315
|
+
* Create a new input method in the appmap with the given name.
|
316
|
+
*
|
317
|
+
* All following feature defintions will be associated with this app.
|
318
|
+
*
|
319
|
+
* @param inputMethodName the desired input method name
|
320
|
+
* @param description string the description of the input method
|
321
|
+
* @param isActiveFn function that should return true when the screen is currently both visible and accessible
|
322
|
+
* @param selector selector relative to target() that returns this input method
|
323
|
+
* @return this
|
324
|
+
*/
|
325
|
+
appmap.createInputMethod = function(inputMethodName, description, isActiveFn, selector) {
|
326
|
+
appmap.lastInputMethod = newInputMethod(inputMethodName, description, isActiveFn, selector, {});
|
327
|
+
appmap.lastInputMethodName = inputMethodName;
|
328
|
+
appmap.inputMethods[inputMethodName] = appmap.lastInputMethod;
|
329
|
+
return this;
|
330
|
+
};
|
331
|
+
|
332
|
+
/**
|
333
|
+
* whether an input method exists
|
334
|
+
*
|
335
|
+
* @param inputMethodName the input method name to test
|
336
|
+
* @return bool
|
337
|
+
*/
|
338
|
+
appmap.hasInputMethod = function(inputMethodName) {
|
339
|
+
return inputMethodName in appmap.inputMethods;
|
340
|
+
};
|
341
|
+
|
342
|
+
/**
|
343
|
+
* All following feature defintions will be associated with this input method.
|
344
|
+
*
|
345
|
+
* @param inputMethodName the desired input method name
|
346
|
+
* @return this
|
347
|
+
*/
|
348
|
+
appmap.augmentInputMethod = function(inputMethodName) {
|
349
|
+
appmap.lastInputMethodName = inputMethodName;
|
350
|
+
appmap.inputMethods[inputMethodName] = appmap.lastInputMethod;
|
351
|
+
return this;
|
352
|
+
};
|
353
|
+
|
354
|
+
/**
|
355
|
+
* Add a feature function to an input method
|
356
|
+
*
|
357
|
+
* @param featureName string name of the function as it will exist in the input method
|
358
|
+
* @param implementationFunction function that will be the implementation
|
359
|
+
* @return this
|
360
|
+
*/
|
361
|
+
appmap.withFeature = function (featureName, implementationFunction) {
|
362
|
+
appmap.lastInputMethod.features[featureName] = implementationFunction;
|
363
|
+
return this;
|
364
|
+
}
|
365
|
+
|
366
|
+
|
367
|
+
/**
|
368
|
+
* return an array of all defined apps
|
369
|
+
*/
|
370
|
+
appmap.getApps = function () {
|
371
|
+
var ret = [];
|
372
|
+
for (d in appmap.apps) ret.push(d);
|
373
|
+
return ret;
|
374
|
+
};
|
375
|
+
|
376
|
+
/**
|
377
|
+
* return an array of all defined screens for the given app
|
378
|
+
*
|
379
|
+
* @param app the app name
|
380
|
+
*/
|
381
|
+
appmap.getScreens = function (app) {
|
382
|
+
var ret = [];
|
383
|
+
for (s in appmap.apps[app]) ret.push(s);
|
384
|
+
return ret;
|
385
|
+
};
|
386
|
+
|
387
|
+
/**
|
388
|
+
* return an array of all defined input methods
|
389
|
+
*/
|
390
|
+
appmap.getInputMethods = function () {
|
391
|
+
var ret = [];
|
392
|
+
for (s in appmap.inputMethods) ret.push(s);
|
393
|
+
return ret;
|
394
|
+
};
|
395
|
+
|
396
|
+
/**
|
397
|
+
* Return an input method by name
|
398
|
+
*
|
399
|
+
* @param methodName string the name of the input method
|
400
|
+
*/
|
401
|
+
appmap.getInputMethod = function (methodName) {
|
402
|
+
if (methodName in appmap.inputMethods) return appmap.inputMethods[methodName];
|
403
|
+
throw new IlluminatorSetupException("Input method " + methodName + " is not defined. " +
|
404
|
+
"Choices are [" + appmap.getInputMethods().join(",") + "]");
|
405
|
+
}
|
406
|
+
|
407
|
+
/**
|
408
|
+
* Returns Markdown string describing all the apps, screens, targets, actions, implementations, and parameters
|
409
|
+
*/
|
410
|
+
appmap.toMarkdown = function () {
|
411
|
+
// print input methods first, since they are common to all apps
|
412
|
+
var ret = ["The following input methods are defined in the Illuminator AppMap (in addition to the iOS default keyboard):"];
|
413
|
+
for (var k in appmap.inputMethods) {
|
414
|
+
var mth = appmap.inputMethods[k];
|
415
|
+
ret.push("* `" + k + "`: " + mth.description);
|
416
|
+
// print features
|
417
|
+
for (var f in mth.features) {
|
418
|
+
ret.push(" * `" + f + "`");
|
419
|
+
}
|
420
|
+
}
|
421
|
+
|
422
|
+
// formatting the title, making good looking markdown
|
423
|
+
var title = function (rank, text) {
|
424
|
+
// insert blank lines before the title
|
425
|
+
var total = 4;
|
426
|
+
for (var i = 0; i <= (total - rank); ++i) {
|
427
|
+
ret.push("");
|
428
|
+
}
|
429
|
+
|
430
|
+
switch (rank) {
|
431
|
+
case 1:
|
432
|
+
ret.push(text);
|
433
|
+
ret.push(Array(Math.max(10, text.length) + 1).join("="));
|
434
|
+
break;
|
435
|
+
case 2:
|
436
|
+
ret.push(text);
|
437
|
+
ret.push(Array(Math.max(10, text.length) + 1).join("-"));
|
438
|
+
break;
|
439
|
+
default:
|
440
|
+
ret.push(Array(rank + 1).join("#") + " " + text);
|
441
|
+
}
|
442
|
+
};
|
443
|
+
|
444
|
+
// if an action is defined on the same targets as its parent screen (not just a subset)
|
445
|
+
var onSameTargets = function (action) {
|
446
|
+
if ("default" == Object.keys(action.actionFn)[0]) return true;
|
447
|
+
for (d in action.isCorrectScreen) if (undefined === action.actionFn[d]) return false;
|
448
|
+
return true;
|
449
|
+
};
|
450
|
+
|
451
|
+
ret.push("The following apps are defined in the Illuminator AppMap:");
|
452
|
+
|
453
|
+
// iterate over apps
|
454
|
+
var apps = Object.keys(appmap.apps).sort();
|
455
|
+
for (var i = 0; i < apps.length; ++i) {
|
456
|
+
var appName = apps[i];
|
457
|
+
var app = appmap.apps[appName];
|
458
|
+
title(1, appName);
|
459
|
+
ret.push("This app has the following screens:");
|
460
|
+
|
461
|
+
// iterate over screens
|
462
|
+
var screens = Object.keys(app).sort();
|
463
|
+
for (var j = 0; j < screens.length; ++j) {
|
464
|
+
var scnName = screens[j];
|
465
|
+
var scn = app[scnName];
|
466
|
+
title(2, scnName);
|
467
|
+
|
468
|
+
// just use the first action on the screen to get the targets - from isCorrectScreen map
|
469
|
+
var screenTargets = "`" + Object.keys(scn[Object.keys(scn)[0]].isCorrectScreen).join("`, `") + "`";
|
470
|
+
ret.push("Defined for " + screenTargets + ", with the following actions:");
|
471
|
+
ret.push(""); // need blank line before bulleted lists
|
472
|
+
|
473
|
+
// iterate over actions
|
474
|
+
var actions = Object.keys(scn).sort();
|
475
|
+
for (var k = 0; k < actions.length; ++k) {
|
476
|
+
var actName = actions[k];
|
477
|
+
var act = scn[actName];
|
478
|
+
var actionTargets = onSameTargets(act) ? "" : " `" + Object.keys(act.actionFn).join("`, `") + "`";
|
479
|
+
var parms = Object.keys(act.params).length == 0 ? "" : " (parameterized)";
|
480
|
+
ret.push("* **" + actName + "**" + parms + actionTargets + ": " + act.description);
|
481
|
+
|
482
|
+
// iterate over parameters
|
483
|
+
var params = Object.keys(act.params).sort();
|
484
|
+
for (var m = 0; m < params.length; ++m) {
|
485
|
+
var paramName = params[m];
|
486
|
+
var par = act.params[paramName];
|
487
|
+
ret.push(" * `" + paramName + "`" + (par.required ? "" : " (optional)") + ": " + par.description);
|
488
|
+
}
|
489
|
+
}
|
490
|
+
|
491
|
+
}
|
492
|
+
}
|
493
|
+
|
494
|
+
return ret.join("\n");
|
495
|
+
};
|
496
|
+
|
497
|
+
|
498
|
+
// create an action builder to enable easy one-liners for common actions
|
499
|
+
// intended use is to say var ab = appmap.actionBuilder.makeAction;
|
500
|
+
// then in appmap: .withImplementation(ab.verifyElement.visibility({name: "blah"}))
|
501
|
+
appmap.actionBuilder = {};
|
502
|
+
appmap.actionBuilder.makeAction = {};
|
503
|
+
appmap.actionBuilder.makeAction.verifyElement = {};
|
504
|
+
appmap.actionBuilder.makeAction.element = {};
|
505
|
+
appmap.actionBuilder.makeAction.selector = {};
|
506
|
+
appmap.actionBuilder.makeAction.screenIsActive = {};
|
507
|
+
|
508
|
+
|
509
|
+
/**
|
510
|
+
* Internal function for getting child elements and raising the appropriate errors
|
511
|
+
*
|
512
|
+
* @param selector the selector to get the element
|
513
|
+
* @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
|
514
|
+
*/
|
515
|
+
appmap.actionBuilder._getElement = function(selector, retryDelay) {
|
516
|
+
try {
|
517
|
+
return target().getOneChildElement(selector);
|
518
|
+
} catch (e) {
|
519
|
+
// it's possible that the selector returned multiple things, so re-raise that
|
520
|
+
if (!isHardSelector(selector)) {
|
521
|
+
var elems = target().getChildElements(selector);
|
522
|
+
if (Object.keys(getUniqueElements(elems)).length > 1) throw e;
|
523
|
+
}
|
524
|
+
|
525
|
+
// one consequence-free failure allowed if retryDelay was specified
|
526
|
+
if (retryDelay === undefined) throw e;
|
527
|
+
delay(retryDelay);
|
528
|
+
return target().getOneChildElement(selector);
|
529
|
+
}
|
530
|
+
};
|
531
|
+
|
532
|
+
|
533
|
+
/**
|
534
|
+
* Internal function for building actions that verify predicate functions
|
535
|
+
*
|
536
|
+
* @param selector the element to select
|
537
|
+
* @param elemName the name of what is being selected
|
538
|
+
* @param predicateFn the function (taking an element as an argument, returning a boolean) that should be evaluated
|
539
|
+
* @param predicateDesc the name of the predicate function, for logging purposes
|
540
|
+
* @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
|
541
|
+
* @return a function (taking an object with fields {expected: boolean} as its only argument) that asserts the predicate
|
542
|
+
*/
|
543
|
+
appmap.actionBuilder.makeAction.verifyElement._predicate = function(selector, elemName, predicateFn, predicateDesc, retryDelay) {
|
544
|
+
return function(parm) {
|
545
|
+
var elem = appmap.actionBuilder._getElement(selector, retryDelay);
|
546
|
+
|
547
|
+
// prevent any funny business with integer comparisons
|
548
|
+
if ((predicateFn(elem) == true) != (parm.expected == true)) {
|
549
|
+
throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed predicate " + predicateDesc
|
550
|
+
+ " (expected value: " + parm.expected + ")");
|
551
|
+
}
|
552
|
+
};
|
553
|
+
};
|
554
|
+
|
555
|
+
|
556
|
+
/**
|
557
|
+
* Internal function for building actions that interact with elements
|
558
|
+
*
|
559
|
+
* @param selector the element to select
|
560
|
+
* @param elemName the name of what is being selected
|
561
|
+
* @param workFn the function (taking an element as an argument) that should be performed
|
562
|
+
* @param retryDelay optional integer, if provided and the selector fails then the selector will be retried
|
563
|
+
* @param return a function (taking no arguments) that executes the work function on the selected element
|
564
|
+
*/
|
565
|
+
appmap.actionBuilder.makeAction.element._interaction = function(selector, elemName, workFn, retryDelay) {
|
566
|
+
return function(parm) {
|
567
|
+
var elem = appmap.actionBuilder._getElement(selector, retryDelay);
|
568
|
+
workFn(elem, parm);
|
569
|
+
};
|
570
|
+
};
|
571
|
+
|
572
|
+
|
573
|
+
|
574
|
+
/**
|
575
|
+
* create a screenIsActive function by looking for a specific element.
|
576
|
+
*
|
577
|
+
* @param screenName the screen name, for logging purposes
|
578
|
+
* @param elementName what is being selected, for logging purposes
|
579
|
+
* @param selector the selector to watch
|
580
|
+
* @param timeout how long to wait
|
581
|
+
* @return a no-argument function that returns a boolean
|
582
|
+
*/
|
583
|
+
appmap.actionBuilder.makeAction.screenIsActive.byElement = function (screenName, elementName, selector, timeout) {
|
584
|
+
switch (typeof timeout) {
|
585
|
+
case "number": break;
|
586
|
+
default: throw new appmap.SetupException("makeAction.screenIsActive.byElement got a bad timeout value: (" + (typeof timeout) + ") " + timeout);
|
587
|
+
}
|
588
|
+
|
589
|
+
return function () {
|
590
|
+
try {
|
591
|
+
target().waitForChildExistence(timeout, true, elementName, selector);
|
592
|
+
return true;
|
593
|
+
} catch (e) {
|
594
|
+
UIALogger.logDebug("screenIsActive.byElement function for " + screenName + " got error: " + e);
|
595
|
+
return false;
|
596
|
+
}
|
597
|
+
};
|
598
|
+
};
|
599
|
+
|
600
|
+
|
601
|
+
/**
|
602
|
+
* build an action function that verifies whether an element is enabled
|
603
|
+
*
|
604
|
+
* @param selector reference to the element
|
605
|
+
* @param elemName the name of what we are looking for
|
606
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
607
|
+
* @return a function that takes an object with fields {expected: boolean} as its argument
|
608
|
+
*/
|
609
|
+
appmap.actionBuilder.makeAction.verifyElement.enabled = function(selector, elemName, retryDelay) {
|
610
|
+
return appmap.actionBuilder.makeAction.verifyElement._predicate(selector, elemName, function (elem) {
|
611
|
+
return elem.isEnabled();
|
612
|
+
}, "isEnabled", retryDelay);
|
613
|
+
};
|
614
|
+
|
615
|
+
|
616
|
+
/**
|
617
|
+
* build an action function that verifies element existence
|
618
|
+
*
|
619
|
+
* @param selector reference to the element
|
620
|
+
* @param elemName the name of what we are looking for
|
621
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
622
|
+
* @return a function that takes an object with fields {expected: boolean} as its argument
|
623
|
+
*/
|
624
|
+
appmap.actionBuilder.makeAction.verifyElement.existence = function(selector, elemName, retryDelay) {
|
625
|
+
return function(parm) {
|
626
|
+
var msg = "";
|
627
|
+
try {
|
628
|
+
var elem = appmap.actionBuilder._getElement(selector, retryDelay);
|
629
|
+
if ((parm.expected === true) == isNotNilElement(elem)) return;
|
630
|
+
} catch (e) {
|
631
|
+
msg = ": " + e.toString();
|
632
|
+
if (!(parm.expected === true)) return;
|
633
|
+
}
|
634
|
+
throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed existence check "
|
635
|
+
+ "(expected: " + parm.expected + ")" + msg);
|
636
|
+
};
|
637
|
+
};
|
638
|
+
|
639
|
+
|
640
|
+
/**
|
641
|
+
* build an action function that verifies element visibility
|
642
|
+
*
|
643
|
+
* @param selector reference to the element
|
644
|
+
* @param elemName the name of what we are looking for
|
645
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
646
|
+
* @return a function that takes an object with fields {expected: boolean} as its argument
|
647
|
+
*/
|
648
|
+
appmap.actionBuilder.makeAction.verifyElement.visibility = function(selector, elemName, retryDelay) {
|
649
|
+
return function(parm) {
|
650
|
+
var msg = "";
|
651
|
+
try {
|
652
|
+
var elem = appmap.actionBuilder._getElement(selector, retryDelay);
|
653
|
+
if ((elem && elem.isVisible()) == parm.expected) return;
|
654
|
+
} catch (e) {
|
655
|
+
msg = ": " + e.toString();
|
656
|
+
if (!(parm.expected === true)) return;
|
657
|
+
}
|
658
|
+
throw new IlluminatorRuntimeVerificationException("Element " + elemName + " failed visibility check "
|
659
|
+
+ "(expected: " + parm.expected + ")" + msg);
|
660
|
+
};
|
661
|
+
};
|
662
|
+
|
663
|
+
/**
|
664
|
+
* build an action function that verifies element editability
|
665
|
+
*
|
666
|
+
* @param selector reference to the element
|
667
|
+
* @param elemName the name of what we are looking for
|
668
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
669
|
+
* @return a function that takes an object with fields {expected: boolean} as its argument
|
670
|
+
*/
|
671
|
+
appmap.actionBuilder.makeAction.verifyElement.editability = function(selector, elemName, retryDelay) {
|
672
|
+
return appmap.actionBuilder.makeAction.verifyElement._predicate(selector, elemName, function (elem) {
|
673
|
+
return elem.checkIsEditable();
|
674
|
+
}, "isEditable", retryDelay);
|
675
|
+
};
|
676
|
+
|
677
|
+
/**
|
678
|
+
* build an action function that taps an element
|
679
|
+
*
|
680
|
+
* @param selector reference to the element
|
681
|
+
* @param elemName the name of what we are looking for
|
682
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
683
|
+
* @return a no-argument function
|
684
|
+
*/
|
685
|
+
appmap.actionBuilder.makeAction.element.tap = function(selector, elemName, retryDelay) {
|
686
|
+
return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
|
687
|
+
elem.tap()
|
688
|
+
}, retryDelay);
|
689
|
+
};
|
690
|
+
|
691
|
+
/**
|
692
|
+
* build an action function that vtaps an element
|
693
|
+
*
|
694
|
+
* @param selector reference to the element
|
695
|
+
* @param elemName the name of what we are looking for
|
696
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
697
|
+
* @return a no-argument function
|
698
|
+
*/
|
699
|
+
appmap.actionBuilder.makeAction.element.vtap = function(selector, elemName, retryDelay) {
|
700
|
+
return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
|
701
|
+
elem.vtap(4)
|
702
|
+
}, retryDelay);
|
703
|
+
};
|
704
|
+
|
705
|
+
/**
|
706
|
+
* build an action function that svtaps an element
|
707
|
+
*
|
708
|
+
* @param selector reference to the element
|
709
|
+
* @param elemName the name of what we are looking for
|
710
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
711
|
+
* @return a no-argument function
|
712
|
+
*/
|
713
|
+
appmap.actionBuilder.makeAction.element.svtap = function(selector, elemName, retryDelay) {
|
714
|
+
return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
|
715
|
+
elem.svtap(4)
|
716
|
+
}, retryDelay);
|
717
|
+
};
|
718
|
+
|
719
|
+
/**
|
720
|
+
* build an action function that types text in an element
|
721
|
+
*
|
722
|
+
* @param selector reference to the element
|
723
|
+
* @param elemName the name of what we are looking for
|
724
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
725
|
+
* @return a function that takes an object with fields {text: string, clear: boolean} as its argument
|
726
|
+
*/
|
727
|
+
appmap.actionBuilder.makeAction.element.typeString = function(selector, elemName, retryDelay) {
|
728
|
+
return appmap.actionBuilder.makeAction.element._interaction(selector, elemName, function (elem, parm) {
|
729
|
+
elem.typeString(parm.text, parm.clear === true);
|
730
|
+
}, retryDelay);
|
731
|
+
};
|
732
|
+
|
733
|
+
/**
|
734
|
+
* build an action function that checks the existence of a child selector
|
735
|
+
*
|
736
|
+
* @param retryDelay optional integer how long to wait before a retry if selector fails
|
737
|
+
* @param parentSelector reference to the parent element
|
738
|
+
* @return a function that takes an object with fields {selector: selector} as its argument
|
739
|
+
*/
|
740
|
+
appmap.actionBuilder.makeAction.selector.verifyExists = function(retryDelay, parentSelector) {
|
741
|
+
return function(parm) {
|
742
|
+
var fullSelector;
|
743
|
+
if (undefined === parentSelector || !(parentSelector) || 0 == Object.keys(parentSelector).length) {
|
744
|
+
fullSelector = parm.selector;
|
745
|
+
} else {
|
746
|
+
fullSelector = parentSelector;
|
747
|
+
fullSelector.push(parm.selector);
|
748
|
+
}
|
749
|
+
|
750
|
+
//TODO: possibly cache the parentSelector element and use that instead of target()
|
751
|
+
|
752
|
+
var elemsObj = target().getChildElements(fullSelector);
|
753
|
+
// one retry allowed
|
754
|
+
if (Object.keys(elemsObj).length == 0 && retryDelay !== undefined && retryDelay >= 0) {
|
755
|
+
elemsObj = target().getChildElements(fullSelector);
|
756
|
+
}
|
757
|
+
|
758
|
+
// react to number of elements
|
759
|
+
switch (Object.keys(elemsObj).length) {
|
760
|
+
case 0: throw new IlluminatorRuntimeFailureException("Selector found no elements: " + JSON.stringify(fullSelector));
|
761
|
+
case 1: return;
|
762
|
+
default: UIALogger.logMessage("Selector (for existence) found multiple elements: " + JSON.stringify(elemsObj));
|
763
|
+
}
|
764
|
+
};
|
765
|
+
};
|
766
|
+
|
767
|
+
}).call(this);
|