smart_monkey 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +17 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +77 -0
  6. data/Rakefile +57 -0
  7. data/Troubleshooting.md +61 -0
  8. data/VERSION +1 -0
  9. data/bin/smart_monkey +53 -0
  10. data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
  11. data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/lib/bootstrap/css/bootstrap.css +6167 -0
  13. data/lib/bootstrap/css/bootstrap.min.css +9 -0
  14. data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
  15. data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
  16. data/lib/bootstrap/js/bootstrap.js +2280 -0
  17. data/lib/bootstrap/js/bootstrap.min.js +6 -0
  18. data/lib/ios_device_log/deviceconsole +0 -0
  19. data/lib/smart_monkey.rb +2 -0
  20. data/lib/smart_monkey/command_helper.rb +71 -0
  21. data/lib/smart_monkey/monkey_runner.rb +549 -0
  22. data/lib/smart_monkey/templates/automation_result.xsl +61 -0
  23. data/lib/smart_monkey/templates/index.html.erb +77 -0
  24. data/lib/smart_monkey/templates/result.html.erb +110 -0
  25. data/lib/smart_monkey/templates/result_view.coffee +160 -0
  26. data/lib/smart_monkey/templates/result_view.js +250 -0
  27. data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
  28. data/lib/ui-auto-monkey/custom.js +73 -0
  29. data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
  30. data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
  31. data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
  32. data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
  33. data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
  34. data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
  35. data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
  36. data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
  37. data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
  38. data/lib/ui-auto-monkey/tuneup/test.js +71 -0
  39. data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
  40. data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
  41. data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
  42. data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
  43. data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
  44. data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
  45. data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
  46. data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
  47. data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
  48. data/smart_monkey.gemspec +112 -0
  49. data/spec/spec_helper.rb +12 -0
  50. metadata +192 -0
@@ -0,0 +1,6 @@
1
+ #import "assertions.js"
2
+ #import "lang-ext.js"
3
+ #import "uiautomation-ext.js"
4
+ #import "screen.js"
5
+ #import "test.js"
6
+ #import "image_assertion.js"
@@ -0,0 +1,52 @@
1
+ Pod::Spec.new do |spec|
2
+ spec.name = 'tuneup_js'
3
+ spec.version = '1.2.1'
4
+ spec.license = { :type => 'MIT' }
5
+ spec.homepage = 'http://www.tuneupjs.org'
6
+ spec.authors = {
7
+ 'Alex Vollmer' => 'alex.vollmer@gmail.com',
8
+ 'Aaron London' => 'github@postmechanical.com',
9
+ 'Anders Pikas' => 'anders@pikas.se',
10
+ 'BAM' => 'brianm@anytimefitness.com',
11
+ 'BJ Homer' => 'bjhomer@gmail.com',
12
+ 'Brandon Fosdick' => 'bfoz@bfoz.net',
13
+ 'Bruce Gordon' => 'bruceg@yahoo-inc.com',
14
+ 'Faisal Memon' => 'faisalmemonuk@yahoo.com',
15
+ 'Genki Uehara' => 'uehara@cyberdom.co.jp',
16
+ 'Ian Katz' => 'iakatz@paypal.com',
17
+ 'Ian' => 'ifreecarve@gmail.com',
18
+ 'Jerry Jaskierny' => 'jaskiern@channelsix.org',
19
+ 'Kevin Connor' => 'kconnor@book.com',
20
+ 'Thomas Deegan' => 'tdeegan@yahoo-inc.com',
21
+ 'Luke Deniston' => 'luke@agrian.com',
22
+ 'Max Metral' => 'mmetral@paypal.com',
23
+ 'Max Rabin' => 'max@glidetalk.com',
24
+ 'Michael Kamphausen' => 'michael.kamphausen@sinnerschrader.com',
25
+ 'Mirko Beine' => 'mirko.beine@arsinventionis.de',
26
+ 'Ondrej Benes' => 'ondrej.benes@cleverlance.com',
27
+ 'PokerChang' => 'chang@pokerchang.com',
28
+ 'Rob Booth' => 'rob.o.booth@gmail.com',
29
+ 'Ross Sharrott' => 'ross@longweekendmobile.com',
30
+ 'Sebastian Ludwig' => 'sebastian@lurado.de',
31
+ 'Stefan Haubold' => 'stefan.haubold@hait.de',
32
+ 'Steve Moser' => 'smoser2@illinois.edu',
33
+ 'Tony Mann' => 'tmann@thefind.com',
34
+ 'Werner Huber' => 'huber.werner@me.com',
35
+ 'ap4y' => 'lod@pisem.net',
36
+ 'giginet' => 'giginet.net@gmail.com',
37
+ 'hborders' => 'hborders@mail.win.org',
38
+ 'julienp' => 'julien@caffeine.lu',
39
+ 'kimiyash' => 'kimiyash@gmail.com',
40
+ 'kviksilver' => 'berceg@paypal.com',
41
+ 'mah' => 'matthias.hochgatterer@runtastic.com',
42
+ 'rpranata' => 'rendy.pranata@gmail.com',
43
+ 'shifu' => 'chang@pokerchang.com'
44
+ }
45
+ spec.summary = 'A JavaScript library to ease automated iOS UI testing with UIAutomation and Instruments.'
46
+ spec.source = {
47
+ :git => 'https://github.com/alexvollmer/tuneup_js.git',
48
+ :tag => '1.2.1'
49
+ }
50
+ spec.source_files = '*.js', 'image_asserter', 'image_assertion.rb', 'test_runner/*'
51
+ spec.requires_arc = false
52
+ end
@@ -0,0 +1,965 @@
1
+ #import "assertions.js";
2
+ #import "lang-ext.js";
3
+
4
+ //We cannot instantiate a UIAElementNil and still get our extensions, so we hold onto one.
5
+ UIAElementNilSingleton = UIATarget.localTarget().frontMostApp().mainWindow().staticTexts().firstWithName("notFoundx123hfhfhfhfhfhed");
6
+
7
+ extend(UIATableView.prototype, {
8
+ /**
9
+ * A shortcut for:
10
+ * this.cells().firstWithName(name)
11
+ */
12
+ cellNamed: function (name) {
13
+ return this.cells().firstWithName(name);
14
+ },
15
+
16
+ /**
17
+ * Asserts that this table has a cell with the name (accessibility label)
18
+ * matching the given +name+ argument.
19
+ */
20
+ assertCellNamed: function (name) {
21
+ assertNotNull(this.cellNamed(name), "No table cell found named '" + name + "'");
22
+ }
23
+ });
24
+
25
+ var isNotNil = function () {
26
+ var ret = undefined !== this && null != this && this.toString() != "[object UIAElementNil]";
27
+ return ret;
28
+ };
29
+
30
+ extend(UIAElementArray.prototype, {
31
+ /**
32
+ * Same as withName, but takes a regular expression
33
+ */
34
+ withNameRegex: function(pattern) {
35
+ var ret = [];
36
+ for (var i = 0; i < this.length; ++i) {
37
+ var elem = this[i];
38
+ if (elem.isNotNil && elem.isNotNil() && elem.name().match(pattern) !== null) {
39
+ ret.push(elem);
40
+ }
41
+ }
42
+ return ret;
43
+ },
44
+
45
+ /**
46
+ * Same as firstWithName, but takes a regular expression
47
+ */
48
+ firstWithNameRegex: function(pattern) {
49
+ for (var i = 0; i < this.length; ++i) {
50
+ var elem = this[i];
51
+ if (elem.isNotNil && elem.isNotNil() && elem.name().match(pattern) !== null) return elem;
52
+ }
53
+ return new UIAElementNil();
54
+ }
55
+ });
56
+
57
+ extend(UIAElement.prototype, {
58
+
59
+ /**
60
+ * Creates a screenshot of the UIElement and saves it to the log directory with the given name
61
+ */
62
+ captureWithName: function(capture_name) {
63
+ var target = UIATarget.localTarget();
64
+ target.captureRectWithName(this.rect(), capture_name);
65
+ },
66
+ /**
67
+ * Equality operator
68
+ *
69
+ * Properly detects equality of 2 UIAElement objects
70
+ * - Can return false positives if 2 elements (and ancestors) have the same name, type, and rect()
71
+ */
72
+ equals: function(elem2, maxRecursion) {
73
+ maxRecursion = maxRecursion === undefined ? -1 : maxRecursion;
74
+ if (this == elem2) return true; // shortcut when x == x
75
+ if (null === elem2) return false; // shortcut when one is nil
76
+ if (!this.isNotNil() || !elem2.isNotNil()) return !this.isNotNil() && !elem2.isNotNil(); // both nil or neither
77
+ if (this.toString() != elem2.toString()) return false; // element type
78
+ if (this.name() != elem2.name()) return false;
79
+ if (JSON.stringify(this.rect()) != JSON.stringify(elem2.rect())) return false; // possible false positives!
80
+ if (0 == maxRecursion) return true; // stop recursing?
81
+ if (-100 == maxRecursion) UIALogger.logWarning("Passed 100 recursions in UIAElement.equals");
82
+ return this.parent() === null || this.parent().equals(elem2.parent(), maxRecursion - 1); // check parent elem
83
+ },
84
+
85
+ /**
86
+ * General-purpose reduce function
87
+ *
88
+ * Applies the callback function to each node in the element tree starting from the current element.
89
+ *
90
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
91
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
92
+ * currentValue is the UIAElement at the current location in the tree
93
+ * accessor_prefix is the code to access this element from the toplevel element
94
+ * toplevel is the top-level element on which this reduce function was called
95
+ *
96
+ * visibleOnly prunes the search tree to visible elements only
97
+ */
98
+ _reduce: function(callback, initialValue, visibleOnly) {
99
+ var reduce_helper = function (elem, acc, prefix) {
100
+ var scalars = ["navigationBar", "popover", "tabBar", "toolbar"];
101
+ var vectors = ["activityIndicators", "buttons", "cells", "collectionViews", "images", "links", "navigationBars",
102
+ "pageIndicators", "pickers", "progressIndicators", "scrollViews", "searchBars",
103
+ "secureTextFields", "segmentedControls", "sliders", "staticTexts", "switches", "tabBars",
104
+ "tableViews", "textFields", "textViews", "toolbars", "webViews"];
105
+
106
+ // function to visit an element, and add it to an array of what was discovered
107
+ var accessed = [];
108
+ var visit = function(someElem, accessor, onlyConsiderNew) {
109
+ // filter invalid
110
+ if (undefined === someElem) return;
111
+ if (!someElem.isNotNil()) return;
112
+
113
+ // filter already visited (in cases where we care)
114
+ if (onlyConsiderNew) {
115
+ for (var i = 0; i < accessed.length; ++i) {
116
+ if (accessed[i].equals(someElem, 0)) return;
117
+ }
118
+ }
119
+ accessed.push(someElem);
120
+
121
+ // filter based on visibility
122
+ if (visibleOnly && !someElem.isVisible()) return;
123
+ acc = reduce_helper(someElem, callback(acc, someElem, accessor, this), accessor);
124
+ };
125
+
126
+ // try to access an element by name instead of number
127
+ var getNamedIndex = function(someArray, numericIndex) {
128
+ var e = someArray[numericIndex];
129
+ var name = e.name();
130
+ if (name !== null && e.equals(someArray.firstWithName(name), 0)) return '"' + name + '"';
131
+ return numericIndex;
132
+ }
133
+
134
+ // visit scalars
135
+ for (var i = 0; i < scalars.length; ++i) {
136
+ visit(elem[scalars[i]](), prefix + "." + scalars[i] + "()", false);
137
+ }
138
+
139
+ // visit the elements of the vectors
140
+ for (var i = 0; i < vectors.length; ++i) {
141
+ if (undefined === elem[vectors[i]]) continue;
142
+ var elemArray = elem[vectors[i]]();
143
+ if (undefined === elemArray) continue;
144
+ for (var j = 0; j < elemArray.length; ++j) {
145
+ var newElem = elemArray[j];
146
+ visit(newElem, prefix + "." + vectors[i] + "()[" + getNamedIndex(elemArray, j) + "]", false);
147
+ }
148
+ }
149
+
150
+ // visit any un-visited items
151
+ var elemArray = elem.elements()
152
+ for (var i = 0; i < elemArray.length; ++i) {
153
+ visit(elemArray[i], prefix + ".elements()[" + getNamedIndex(elemArray, i) + "]", true);
154
+ }
155
+ return acc;
156
+ };
157
+
158
+ UIATarget.localTarget().pushTimeout(0);
159
+ try {
160
+ return reduce_helper(this, initialValue, "");
161
+ } catch(e) {
162
+ throw e;
163
+ } finally {
164
+ UIATarget.localTarget().popTimeout();
165
+ }
166
+
167
+ },
168
+
169
+ /**
170
+ * Reduce function
171
+ *
172
+ * Applies the callback function to each node in the element tree starting from the current element.
173
+ *
174
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
175
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
176
+ * currentValue is the UIAElement at the current location in the tree
177
+ * accessor_prefix is the code to access this element from the toplevel element
178
+ * toplevel is the top-level element on which this reduce function was called
179
+ */
180
+ reduce: function(callback, initialValue) {
181
+ return this._reduce(callback, initialValue, false);
182
+ },
183
+
184
+ /**
185
+ * Reduce function
186
+ *
187
+ * Applies the callback function to each visible node in the element tree starting from the current element.
188
+ *
189
+ * Callback function takes (previousValue, currentValue <UIAElement>, accessor_prefix, toplevel <UIAElement>)
190
+ * where previousValue is: initialValue (first time), otherwise the previous return from the callback
191
+ * currentValue is the UIAElement at the current location in the tree
192
+ * accessor_prefix is the code to access this element from the toplevel element
193
+ * toplevel is the top-level element on which this reduce function was called
194
+ */
195
+ reduceVisible: function(callback, initialValue) {
196
+ return this._reduce(callback, initialValue, true);
197
+ },
198
+
199
+ /**
200
+ * Find function
201
+ *
202
+ * Find elements by given criteria
203
+ *
204
+ * Return associative array {accessor: element} of results
205
+ */
206
+ find: function(criteria, varName) {
207
+ if (criteria === undefined) {
208
+ UIALogger.logWarning("No criteria passed to find function, so assuming {} and returning all elements");
209
+ criteria = {};
210
+ }
211
+ varName = varName === undefined ? "<root element>" : varName;
212
+ var visibleOnly = criteria.isVisible === true;
213
+
214
+ var knownOptions = {UIAtype: 1, rect: 1, hasKeyboardFocus: 1, isEnabled: 1, isValid: 1,
215
+ label: 1, name: 1, nameRegex: 1, value: 1};
216
+
217
+ // helpful check, mostly catching capitalization errors
218
+ for (var k in criteria) {
219
+ if (knownOptions[k] === undefined) {
220
+ UIALogger.logWarning(this.toString() + ".find() received unknown criteria field '" + k + "' "
221
+ + "(known fields are " + Object.keys(knownOptions).join(", ") + ")");
222
+
223
+ }
224
+ }
225
+
226
+ var c = criteria;
227
+ var collect_fn = function(acc, elem, prefix, _) {
228
+ if (c.UIAtype !== undefined && "[object " + c.UIAtype + "]" != elem.toString()) return acc;
229
+ if (c.rect !== undefined && JSON.stringify(c.rect) != JSON.stringify(elem.rect())) return acc;
230
+ if (c.hasKeyboardFocus !== undefined && c.hasKeyboardFocus != elem.hasKeyboardFocus()) return acc;
231
+ if (c.isEnabled !== undefined && c.isEnabled != elem.isEnabled()) return acc;
232
+ if (c.isValid !== undefined && c.isValid !== elem.isValid()) return acc;
233
+ if (c.label !== undefined && c.label != elem.label()) return acc;
234
+ if (c.name !== undefined && c.name != elem.name()) return acc;
235
+ if (c.nameRegex !== undefined && (elem.name() === null || elem.name().match(c.nameRegex) === null)) return acc;
236
+ if (c.value !== undefined && c.value != elem.value()) return acc;
237
+
238
+ acc[varName + prefix] = elem;
239
+ return acc;
240
+ }
241
+
242
+ return this._reduce(collect_fn, {}, visibleOnly);
243
+ },
244
+
245
+ /**
246
+ * Dump tree in .js format for copy/paste use in code
247
+ * varname is used as the first element in the canonical name
248
+ */
249
+ elementAccessorDump: function(varName, visibleOnly) {
250
+ varName = varName === undefined ? "<root element>" : varName;
251
+ var title = "elementAccessorDump";
252
+ if (visibleOnly === true) {
253
+ title += " (of visible elements)";
254
+ if (!this.isVisible()) return title + ": <none, " + varName + " is not visible>";
255
+ }
256
+
257
+ var collect_fn = function (acc, _, prefix, __) {
258
+ acc.push(varName + prefix)
259
+ return acc;
260
+ };
261
+
262
+ return this._reduce(collect_fn, [title + " of " + varName + ":", varName], visibleOnly).join("\n");
263
+ },
264
+
265
+ /**
266
+ * Dump tree in json format for copy/paste use in AssertWindow and friends
267
+ */
268
+ elementJSONDump: function (recursive, attributes, visibleOnly) {
269
+ if (visibleOnly && !this.isVisible()) {
270
+ return "";
271
+ }
272
+
273
+ if (!attributes) {
274
+ attributes = ["name", "label", "value", "isVisible"];
275
+ }
276
+ else if (attributes == 'ALL') {
277
+ attributes = ["name",
278
+ "label",
279
+ "value"
280
+ ].concat(getMethods(this).filter(function (method) {
281
+ return method.match(/^(is|has)/)
282
+ }));
283
+ }
284
+
285
+ var jsonStr = "";
286
+ attributes.forEach(function (attr) {
287
+ try {
288
+ var value = this[attr]();
289
+ if (value != null) { //don't print null values
290
+ var valueType = typeof (value);
291
+ //quote strings and numbers. true/false unquoted.
292
+ if (valueType == "string" || valueType == "number") {
293
+ value = "'" + value + "'";
294
+ }
295
+ jsonStr += attr + ': ' + value + ',\n';
296
+ }
297
+ }
298
+ catch (e) {}
299
+ }, this);
300
+
301
+ if (recursive) {
302
+ var children = this.elements().toArray();
303
+ if (children.length > 0) {
304
+ var curType = null;
305
+ children.sort().forEach(function (child) {
306
+
307
+ function elementTypeToUIAGetter(elementType, parent) {
308
+
309
+ //almost all types follow a simple name to getter convention.
310
+ //UIAImage => images. UIAWindow => windows.
311
+ var getter = elementType.substring(3).lcfirst() + 's';
312
+ if (elementType == "UIACollectionCell" || elementType == "UIATableCell") {
313
+ getter = "cells";
314
+ }
315
+ if (parent && !eval('parent.' + getter)) {
316
+ //Note: we can't use introspection to list valid methods on the parents
317
+ //because they are all "native" methods and aren't visible.
318
+ //so the valid getter must be looked up in the documentation and mapped above
319
+ UIALogger.logError("elementTypeToUIAGetter could not determine getter for " + elementType);
320
+ }
321
+ return elementType.substring(3).lcfirst() + 's';
322
+ }
323
+
324
+ var objType = Object.prototype.toString.call(child); //[object UIAWindow]
325
+ objType = objType.substring(8, objType.length - 1); //UIAWindow
326
+ // there's a bug that causes leaf elements to have child references
327
+ // back up to UIAApplication, thus the check for that
328
+ // this means we can't dump from the "target" level - only mainWindow and below
329
+ // hopefully this bug goes away 2013-07-02
330
+ if (objType == "UIAApplication" || objType == "UIAElementNil" || (visibleOnly && !child.isVisible())) {
331
+ //skip this child
332
+ return;
333
+ }
334
+
335
+ if (objType == "UIACollectionCell" && !this.isVisible()) {
336
+ //elements() shows invisible cells that cells() does not
337
+ return;
338
+ }
339
+ if (curType && curType != objType) {
340
+ //close off open list
341
+ jsonStr += "],\n";
342
+ }
343
+ if (!curType || curType != objType) {
344
+ curType = objType;
345
+ //open a new list
346
+ jsonStr += elementTypeToUIAGetter(objType, this) + ": [\n";
347
+ }
348
+
349
+ var childJsonStr = child.elementJSONDump(true, attributes, visibleOnly);
350
+ if (childJsonStr) {
351
+ jsonStr += "{\n";
352
+ jsonStr += childJsonStr.replace(/^/gm, " ").replace(/ $/, '');
353
+ jsonStr += "},\n";
354
+ }
355
+ else {
356
+ //child has no attributes to report (all null)
357
+ jsonStr += " null,\n";
358
+ }
359
+
360
+ }, this);
361
+ if (curType) {
362
+ //close off open list
363
+ jsonStr += "],\n";
364
+ }
365
+ }
366
+ }
367
+
368
+ return jsonStr;
369
+ },
370
+
371
+ logElementJSON: function (attributes) {
372
+ //TODO dump the path to the object in the debug line
373
+ //ex: target.frontMostApp().mainWindow().toolbars()[0].buttons()["Library"]
374
+ UIALogger.logDebug("logElementJSON: " + (attributes ? "[" + attributes + "]" : '') + "\n" + this.elementJSONDump(false, attributes));
375
+ },
376
+
377
+ logVisibleElementJSON: function (attributes) {
378
+ //TODO dump the path to the object in the debug line
379
+ //ex: target.frontMostApp().mainWindow().toolbars()[0].buttons()["Library"]
380
+ UIALogger.logDebug("logVisibleElementJSON: " + (attributes ? "[" + attributes + "]" : '') + "\n" + this.elementJSONDump(false, attributes, true));
381
+ },
382
+
383
+ logElementTreeJSON: function (attributes) {
384
+ UIALogger.logDebug("logElementTreeJSON: " + (attributes ? "[" + attributes + "]" : '') + "\n" + this.elementJSONDump(true, attributes));
385
+ },
386
+
387
+ logVisibleElementTreeJSON: function (attributes) {
388
+ UIALogger.logDebug("logVisibleElementTreeJSON: " + (attributes ? "[" + attributes + "]" : '') + "\n" + this.elementJSONDump(true, attributes, true));
389
+ },
390
+
391
+
392
+ /**
393
+ * Poll till the item becomes visible, up to a specified timeout
394
+ */
395
+ waitUntilVisible: function (timeoutInSeconds) {
396
+ this.waitUntil(function (element) {
397
+ return element;
398
+ }, function (element) {
399
+ return element.isVisible();
400
+ }, timeoutInSeconds, "to become visible");
401
+ },
402
+
403
+ /**
404
+ * Wait until element becomes invisible
405
+ */
406
+ waitUntilInvisible: function (timeoutInSeconds) {
407
+ this.waitUntil(function (element) {
408
+ return element;
409
+ }, function (element) {
410
+ return !element.isVisible();
411
+ }, timeoutInSeconds, "to become invisible");
412
+ },
413
+
414
+ /**
415
+ * Wait until child element with name is added
416
+ */
417
+ waitUntilFoundByName: function (name, timeoutInSeconds) {
418
+ this.waitUntil(function (element) {
419
+ return element.elements().firstWithName(name);
420
+ }, function (element) {
421
+ return element.isValid();
422
+ }, timeoutInSeconds, ["to become valid (with name '", name, "')"].join(""));
423
+ },
424
+
425
+ /**
426
+ * Wait until child element with name is removed
427
+ */
428
+ waitUntilNotFoundByName: function (name, timeoutInSeconds) {
429
+ this.waitUntil(function (element) {
430
+ return element.elements().firstWithName(name);
431
+ }, function (element) {
432
+ return !element.isValid();
433
+ }, timeoutInSeconds, ["to become invalid (with name '", name, "'')"].join(""));
434
+ },
435
+
436
+
437
+ /**
438
+ * Wait until lookup_function(this) returns a valid lookup
439
+ * For convenience, return the element that was found
440
+ * Allow a label for more helpful error messages
441
+ */
442
+ waitUntilAccessorSuccess: function (lookup_function, timeoutInSeconds, label) {
443
+ var isNotUseless = function (elem) {
444
+ return elem !== null && elem.isNotNil();
445
+ }
446
+
447
+ // this function will be referenced in waitUntil -- it supplies
448
+ // the name of what we are waiting for
449
+ var label_fn = function () {
450
+ return label;
451
+ }
452
+
453
+ if (!isNotUseless(this)) {
454
+ throw "waitUntilAccessorSuccess: won't work because the top element isn't valid";
455
+ }
456
+
457
+ this.waitUntil(function (element) {
458
+ // annotate the found elements with the label function if they are nil
459
+ try {
460
+ var possibleMatch = lookup_function(element);
461
+ if (!possibleMatch.isNotNil() && label !== undefined) possibleMatch.label = label_fn;
462
+ return possibleMatch;
463
+ }
464
+ catch (e) {
465
+ var fakeNil = new UIAElementNil();
466
+ if (label !== undefined) fakeNil.label = label_fn;
467
+ return fakeNil;
468
+ }
469
+ }, isNotUseless,
470
+ timeoutInSeconds, "to become an acceptable return value from the given function");
471
+ return lookup_function(this);
472
+ },
473
+
474
+
475
+ /**
476
+ * Wait until one lookup_function(this) in an associative array of lookup functions
477
+ * returns a valid lookup.
478
+ *
479
+ * Return an associative array of {key: <element found>, elem: <the element that was found>}
480
+ */
481
+ waitUntilAccessorSelect: function (lookup_functions, timeoutInSeconds) {
482
+ var isNotUseless = function (elem) {
483
+ return elem !== null && elem.isNotNil();
484
+ }
485
+
486
+ if (!isNotUseless(this)) {
487
+ throw "waitUntilAccessorSelect: won't work because the top element isn't valid";
488
+ }
489
+
490
+ // composite find function
491
+ var find_any = function (element) {
492
+ for (var k in lookup_functions) {
493
+ var lookup_function = lookup_functions[k];
494
+ try {
495
+ var el = lookup_function(element);
496
+ if (isNotUseless(el)) return {key: k, elem: el};
497
+ }
498
+ catch (e) {
499
+ // ignore
500
+ }
501
+ }
502
+ return UIAElementNilSingleton; //do not create a new UIAElementNil as our prototype extensions are not on that object.
503
+ };
504
+
505
+ //cache find_any() results since we want to use the values later. We don't want to reinvokve find_any() because the UI might have changed since we last found something
506
+ //and if it no longer finds anything then find_any() will return UIAElementNil(), which we would then return, breaking the api contract to return a {key: elem:} object.
507
+ var successfulResult = null;
508
+
509
+ this.waitUntil(function (element) {
510
+ var result = find_any(element);
511
+ if (undefined !== result && result !== UIAElementNilSingleton) { //find_any() will return UIAElementNilSingleton if not found and we don't want to start processing that-especially not result["elem"]
512
+ successfulResult = result;
513
+ return result["elem"];
514
+ }
515
+ // we cannot support annotating the found elements with the label function if they are nil, because we reuse UIAElementNilSingleton
516
+ return UIAElementNilSingleton;
517
+ }, isNotUseless,
518
+ timeoutInSeconds, "to produce any acceptable return values");
519
+ return successfulResult;
520
+ },
521
+
522
+
523
+ /**
524
+ * Wait until the element has the given name
525
+ */
526
+ waitUntilHasName: function (name, timeoutInSeconds) {
527
+
528
+ this.waitUntil(function (element) {
529
+ return element;
530
+ }, function (element) {
531
+ return element.name() == name;
532
+ }, timeoutInSeconds, "to have the name '" + name + "'");
533
+
534
+ },
535
+
536
+
537
+ /**
538
+ * Wait until element fulfills condition
539
+ */
540
+ waitUntil: function (filterFunction, conditionFunction, timeoutInSeconds, description) {
541
+ timeoutInSeconds = timeoutInSeconds == null ? 5 : timeoutInSeconds;
542
+ var element = this;
543
+ var delay = 0.25;
544
+ UIATarget.localTarget().pushTimeout(0);
545
+ try {
546
+ retry(function () {
547
+ var filteredElement = filterFunction(element);
548
+ if (!conditionFunction(filteredElement)) {
549
+ if (!(filteredElement !== null && filteredElement.isNotNil())) {
550
+ var label = (filteredElement && filteredElement.label) ? filteredElement.label() : "Element";
551
+ // make simple error message if the element doesn't exist
552
+ throw ([label, "failed", description,
553
+ "within", timeoutInSeconds, "seconds."
554
+ ].join(" "));
555
+ }
556
+ else {
557
+ // build a detailed error message with all available info on the current element
558
+ var elementDescription = filteredElement.toString();
559
+ if (filteredElement.name !== undefined) {
560
+ var elemName = filteredElement.name();
561
+ if (elemName !== null && elemName != "") {
562
+ elementDescription += " with name '" + elemName + "'";
563
+ }
564
+ }
565
+ throw (["Element", elementDescription,
566
+ "failed", description,
567
+ "within", timeoutInSeconds, "seconds."
568
+ ].join(" "));
569
+ }
570
+ }
571
+ }, Math.max(1, timeoutInSeconds / delay), delay);
572
+ }
573
+ catch (e) {
574
+ throw e;
575
+ }
576
+ finally {
577
+ UIATarget.localTarget().popTimeout();
578
+ }
579
+
580
+ },
581
+
582
+ /**
583
+ * A shortcut for waiting an element to become visible and tap.
584
+ */
585
+ vtap: function (timeout) {
586
+ if (undefined === timeout) timeout = 10;
587
+ this.waitUntilVisible(timeout);
588
+ this.tap();
589
+ },
590
+
591
+ /**
592
+ * A shortcut for scrolling to a visible item and and tap.
593
+ */
594
+ svtap: function (timeout) {
595
+ if (undefined === timeout) timeout = 1;
596
+ try {
597
+ this.scrollToVisible();
598
+ } catch (e) {
599
+ // iOS 6 hack when no scrolling is needed
600
+ if (e.toString() != "scrollToVisible cannot be used on the element because it does not have a scrollable ancestor.") {
601
+ throw e;
602
+ }
603
+ }
604
+ //this.waitUntilVisible(timeout);
605
+ this.tap();
606
+ },
607
+ /**
608
+ * A shortcut for touching an element and waiting for it to disappear.
609
+ */
610
+ tapAndWaitForInvalid: function () {
611
+ this.tap();
612
+ this.waitForInvalid();
613
+ },
614
+
615
+ /**
616
+ * verify that a text field is editable by tapping in it and waiting for a keyboard to appear.
617
+ */
618
+ checkIsEditable: function () {
619
+ try {
620
+ var keyboardWasUp = target.frontMostApp().keyboard().isVisible();
621
+
622
+ // warn user if this is an object that might be destructively or oddly affected by this check
623
+ switch (this.toString()) {
624
+ case "[object UIAButton]":
625
+ case "[object UIALink]":
626
+ case "[object UIAActionSheet]":
627
+ case "[object UIAKey]":
628
+ case "[object UIAKeyboard]":
629
+ UIALogger.logWarning("checkIsEditable is going to tap() an object of type " + this.toString());
630
+ default:
631
+ this.tap();
632
+ }
633
+
634
+ // wait for keyboard to disappear if it was already active
635
+ if (keyboardWasUp) UIATarget.localTarget().delay(0.35);
636
+ target.frontMostApp().keyboard().waitUntilVisible(2);
637
+ return true;
638
+ } catch (e) {
639
+ return false;
640
+ }
641
+ },
642
+
643
+ isNotNil: isNotNil,
644
+ });
645
+
646
+ extend(UIAElementNil.prototype, {
647
+ isNotNil: function () {
648
+ return false;
649
+ },
650
+ isValid: function () {
651
+ return false;
652
+ },
653
+ isVisible: function () {
654
+ return false;
655
+ }
656
+ });
657
+
658
+ extend(UIAApplication.prototype, {
659
+ /**
660
+ * A shortcut for getting the current view controller's title from the
661
+ * navigation bar. If there is no navigation bar, this method returns null
662
+ */
663
+ navigationTitle: function () {
664
+ navBar = this.mainWindow().navigationBar();
665
+ if (navBar) {
666
+ return navBar.name();
667
+ }
668
+ return null;
669
+ },
670
+
671
+ /**
672
+ * A shortcut for checking that the interface orientation in either
673
+ * portrait mode
674
+ */
675
+ isPortraitOrientation: function () {
676
+ var orientation = this.interfaceOrientation();
677
+ return orientation == UIA_DEVICE_ORIENTATION_PORTRAIT ||
678
+ orientation == UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN;
679
+ },
680
+
681
+ /**
682
+ * A shortcut for checking that the interface orientation in one of the
683
+ * landscape orientations.
684
+ */
685
+ isLandscapeOrientation: function () {
686
+ var orientation = this.interfaceOrientation();
687
+ return orientation == UIA_DEVICE_ORIENTATION_LANDSCAPELEFT ||
688
+ orientation == UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT;
689
+ }
690
+ });
691
+
692
+ extend(UIANavigationBar.prototype, {
693
+ /**
694
+ * Asserts that the left button's name matches the given +name+ argument
695
+ */
696
+ assertLeftButtonNamed: function (name) {
697
+ assertEquals(name, this.leftButton().name());
698
+ },
699
+
700
+ /**
701
+ * Asserts that the right button's name matches the given +name+ argument
702
+ */
703
+ assertRightButtonNamed: function (name) {
704
+ assertEquals(name, this.rightButton().name());
705
+ }
706
+ });
707
+
708
+ extend(UIATarget.prototype, {
709
+ /**
710
+ * A shortcut for checking that the interface orientation in either
711
+ * portrait mode
712
+ */
713
+ isPortraitOrientation: function () {
714
+ var orientation = this.deviceOrientation();
715
+ return orientation == UIA_DEVICE_ORIENTATION_PORTRAIT ||
716
+ orientation == UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN;
717
+ },
718
+
719
+ /**
720
+ * A shortcut for checking that the interface orientation in one of the
721
+ * landscape orientations.
722
+ */
723
+ isLandscapeOrientation: function () {
724
+ var orientation = this.deviceOrientation();
725
+ return orientation == UIA_DEVICE_ORIENTATION_LANDSCAPELEFT ||
726
+ orientation == UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT;
727
+ },
728
+
729
+ /**
730
+ * Determine if we are running on a simulator.
731
+ */
732
+ isSimulator: function() {
733
+ return this.model().match(/Simulator/) !== null;
734
+ },
735
+
736
+ /**
737
+ * A convenience method for detecting that you're running on an iPad
738
+ */
739
+ isDeviceiPad: function () {
740
+ //model is iPhone Simulator, even when running in iPad mode
741
+ return this.model().match(/^iPad/) !== null ||
742
+ this.name().match(/iPad Simulator/) !== null;
743
+ },
744
+
745
+ /**
746
+ * A convenience method for detecting that you're running on an
747
+ * iPhone or iPod touch
748
+ */
749
+ isDeviceiPhone: function () {
750
+ return this.model().match(/^iPad/) === null &&
751
+ this.name().match(/^iPad Simulator$/) === null;
752
+ },
753
+
754
+ /**
755
+ * A shortcut for checking if target device is iPhone 5 (or iPod Touch
756
+ * 5th generation)
757
+ */
758
+ isDeviceiPhone5: function () {
759
+ var isIphone = this.isDeviceiPhone();
760
+ var deviceScreen = this.rect();
761
+ return isIphone && deviceScreen.size.height == 568;
762
+ },
763
+
764
+ /**
765
+ * A convenience method for producing screenshots without status bar
766
+ */
767
+ captureAppScreenWithName: function (imageName) {
768
+ var appRect = this.rect();
769
+
770
+ appRect.origin.y += 20.0;
771
+ appRect.size.height -= 20.0;
772
+
773
+ return this.captureRectWithName(appRect, imageName);
774
+ },
775
+
776
+ logDeviceInfo: function () {
777
+ UIALogger.logMessage("Dump Device:");
778
+ UIALogger.logMessage("DeviceInfo - model: " + this.model());
779
+ UIALogger.logMessage("DeviceInfo - rect: " + JSON.stringify(this.rect()));
780
+ UIALogger.logMessage("DeviceInfo - name: " + this.name());
781
+ UIALogger.logMessage("DeviceInfo - systemName: " + this.systemName());
782
+ UIALogger.logMessage("DeviceInfo - systemVersion: " + this.systemVersion());
783
+ }
784
+ });
785
+ extend(UIAKeyboard.prototype, {
786
+ KEYBOARD_TYPE_UNKNOWN: -1,
787
+ KEYBOARD_TYPE_ALPHA: 0,
788
+ KEYBOARD_TYPE_ALPHA_CAPS: 1,
789
+ KEYBOARD_TYPE_NUMBER_AND_PUNCTUATION: 2,
790
+ KEYBOARD_TYPE_NUMBER: 3,
791
+ keyboardType: function () {
792
+ if (this.keys().length < 12) {
793
+ return this.KEYBOARD_TYPE_NUMBER;
794
+ }
795
+ else if (this.keys().firstWithName("a").isNotNil()) {
796
+ return this.KEYBOARD_TYPE_ALPHA;
797
+ }
798
+ else if (this.keys().firstWithName("A").isNotNil()) {
799
+ return this.KEYBOARD_TYPE_ALPHA_CAPS;
800
+ }
801
+ else if (this.keys().firstWithName("1").isNotNil()) {
802
+ return this.KEYBOARD_TYPE_NUMBER_AND_PUNCTUATION;
803
+ }
804
+ else {
805
+ return this.KEYBOARD_TYPE_UNKNOWN;
806
+ }
807
+ }
808
+ });
809
+
810
+ var typeString = function (pstrString, pbClear) {
811
+ pstrString = pstrString.toString();
812
+ // handle keyboard not being focused
813
+ if (!this.hasKeyboardFocus()) {
814
+ this.tap();
815
+ }
816
+ var kb, db; // keyboard, deleteButton
817
+ var seconds = 2;
818
+ var waitTime = 0.25;
819
+ var maxAttempts = seconds / waitTime;
820
+ var noSuccess = true;
821
+ var failMsg = null;
822
+
823
+ // attempt to get a successful keypress several times -- using the first character
824
+ // this is a hack for iOS 6.x where the keyboard is sometimes "visible" before usable
825
+ while ((pbClear || noSuccess) && 0 < maxAttempts--) {
826
+ try {
827
+ kb = target.frontMostApp().keyboard();
828
+ // handle clearing
829
+ if (pbClear) {
830
+ db = kb.buttons()["Delete"];
831
+ if (!db.isNotNil()) db = kb.keys()["Delete"]; // compatibilty hack
832
+
833
+ // touchAndHold doesn't work without this next line... not sure why :(
834
+ db.tap();
835
+ pbClear = false; // prevent clear on next iteration
836
+ db.touchAndHold(3.7);
837
+
838
+ }
839
+
840
+ if (pstrString.length !== 0) {
841
+ kb.typeString(pstrString.charAt(0));
842
+ }
843
+
844
+ noSuccess = false; // here + no error caught means done
845
+ }
846
+ catch (e) {
847
+ failMsg = e;
848
+ UIATarget.localTarget().delay(waitTime);
849
+ }
850
+ }
851
+
852
+ // report any errors that prevented success
853
+ if (0 > maxAttempts && null !== failMsg) throw "typeString caught error: " + failMsg.toString();
854
+
855
+ // now type the rest of the string
856
+ try {
857
+ if (pstrString.length > 0) kb.typeString(pstrString.substr(1));
858
+ } catch (e) {
859
+ if (-1 == e.toString().indexOf(" failed to tap ")) throw e;
860
+
861
+ UIALogger.logDebug("Retrying keyboard action, typing slower this time");
862
+ this.typeString("", true);
863
+ kb.setInterKeyDelay(0.2);
864
+ kb.typeString(pstrString);
865
+ }
866
+
867
+ };
868
+
869
+ extend(UIATextField.prototype, {
870
+ typeString: typeString,
871
+ clear: function () {
872
+ this.typeString("", true);
873
+ }
874
+ });
875
+
876
+ extend(UIATextView.prototype, {
877
+ typeString: typeString,
878
+ clear: function () {
879
+ this.typeString("", true);
880
+ }
881
+ });
882
+
883
+ extend(UIAPickerWheel.prototype, {
884
+
885
+ /*
886
+ * Better implementation than UIAPickerWheel.selectValue
887
+ * Works also for texts
888
+ * Poorly works not for UIDatePickers -> because .values() which get all values of wheel does not work :(
889
+ * I think this is a bug in UIAutomation!
890
+ */
891
+ scrollToValue: function (valueToSelect) {
892
+
893
+ var element = this;
894
+
895
+ var values = this.values();
896
+ var pickerValue = element.value();
897
+
898
+ // convert to string
899
+ valueToSelect = valueToSelect + "";
900
+
901
+ // some wheels return for .value() "17. 128 of 267" ?? don't know why
902
+ // throw away all after "." but be careful lastIndexOf is used because the value can
903
+ // also have "." in it!! e.g.: "1.2. 13 of 27"
904
+ if (pickerValue.lastIndexOf(".") != -1) {
905
+ var currentValue = pickerValue.substr(0, pickerValue.lastIndexOf("."));
906
+ }
907
+ else {
908
+ var currentValue = element.value();
909
+ }
910
+
911
+ var currentValueIndex = values.indexOf(currentValue);
912
+ var valueToSelectIndex = values.indexOf(valueToSelect);
913
+
914
+ if (valueToSelectIndex == -1) {
915
+ fail("value: " + valueToSelect + " not found in Wheel!");
916
+ }
917
+
918
+ var elementsToScroll = valueToSelectIndex - currentValueIndex;
919
+
920
+ UIALogger.logDebug("number of elements to scroll: " + elementsToScroll);
921
+ if (elementsToScroll > 0) {
922
+
923
+ for (i = 0; i < elementsToScroll; i++) {
924
+ element.tapWithOptions({
925
+ tapOffset: {
926
+ x: 0.35,
927
+ y: 0.67
928
+ }
929
+ });
930
+ target.delay(0.7);
931
+ }
932
+
933
+ }
934
+ else {
935
+
936
+ for (i = 0; i > elementsToScroll; i--) {
937
+ element.tapWithOptions({
938
+ tapOffset: {
939
+ x: 0.35,
940
+ y: 0.31
941
+ }
942
+ });
943
+ target.delay(0.7);
944
+ }
945
+ }
946
+ },
947
+
948
+ /*
949
+ * Wheels filled with values return for .value() "17. 128 of 267"
950
+ * ?? don't know why -> for comparisons this is unuseful!!
951
+ * If you want to check a value of a wheel this function is very helpful
952
+ */
953
+ realValue: function () {
954
+
955
+ // current value of wheel
956
+ var pickerValue = this.value();
957
+
958
+ // throw away all after "." but be careful lastIndexOf is used because the value can
959
+ if (pickerValue.lastIndexOf(".") != -1) {
960
+ return pickerValue.substr(0, pickerValue.lastIndexOf("."));
961
+ }
962
+
963
+ return this.value();
964
+ }
965
+ });