UNPKG

31.5 kBJavaScriptView Raw
1/**
2 * All scripts to be run on the client via executeAsyncScript or
3 * executeScript should be put here.
4 *
5 * NOTE: These scripts are transmitted over the wire as JavaScript text
6 * constructed using their toString representation, and *cannot*
7 * reference external variables.
8 *
9 * Some implementations seem to have issues with // comments, so use star-style
10 * inside scripts. (TODO: add issue number / example implementations
11 * that caused the switch to avoid the // comments.)
12 */
13
14// jshint browser: true
15// jshint shadow: true
16/* global angular */
17var functions = {};
18
19///////////////////////////////////////////////////////
20//// ////
21//// HELPERS ////
22//// ////
23///////////////////////////////////////////////////////
24
25
26/* Wraps a function up into a string with its helper functions so that it can
27 * call those helper functions client side
28 *
29 * @param {function} fun The function to wrap up with its helpers
30 * @param {...function} The helper functions. Each function must be named
31 *
32 * @return {string} The string which, when executed, will invoke fun in such a
33 * way that it has access to its helper functions
34 */
35function wrapWithHelpers(fun) {
36 var helpers = Array.prototype.slice.call(arguments, 1);
37 if (!helpers.length) {
38 return fun;
39 }
40 var FunClass = Function; // Get the linter to allow this eval
41 return new FunClass(
42 helpers.join(';') + String.fromCharCode(59) +
43 ' return (' + fun.toString() + ').apply(this, arguments);');
44}
45
46/* Tests if an ngRepeat matches a repeater
47 *
48 * @param {string} ngRepeat The ngRepeat to test
49 * @param {string} repeater The repeater to test against
50 * @param {boolean} exact If the ngRepeat expression needs to match the whole
51 * repeater (not counting any `track by ...` modifier) or if it just needs to
52 * match a substring
53 * @return {boolean} If the ngRepeat matched the repeater
54 */
55function repeaterMatch(ngRepeat, repeater, exact) {
56 if (exact) {
57 return ngRepeat.split(' track by ')[0].split(' as ')[0].split('|')[0].
58 split('=')[0].trim() == repeater;
59 } else {
60 return ngRepeat.indexOf(repeater) != -1;
61 }
62}
63
64/* Tries to find $$testability and possibly $injector for an ng1 app
65 *
66 * By default, doesn't care about $injector if it finds $$testability. However,
67 * these priorities can be reversed.
68 *
69 * @param {string=} selector The selector for the element with the injector. If
70 * falsy, tries a variety of methods to find an injector
71 * @param {boolean=} injectorPlease Prioritize finding an injector
72 * @return {$$testability?: Testability, $injector?: Injector} Returns whatever
73 * ng1 app hooks it finds
74 */
75function getNg1Hooks(selector, injectorPlease) {
76 function tryEl(el) {
77 try {
78 if (!injectorPlease && angular.getTestability) {
79 var $$testability = angular.getTestability(el);
80 if ($$testability) {
81 return {$$testability: $$testability};
82 }
83 } else {
84 var $injector = angular.element(el).injector();
85 if ($injector) {
86 return {$injector: $injector};
87 }
88 }
89 } catch(err) {}
90 }
91 function trySelector(selector) {
92 var els = document.querySelectorAll(selector);
93 for (var i = 0; i < els.length; i++) {
94 var elHooks = tryEl(els[i]);
95 if (elHooks) {
96 return elHooks;
97 }
98 }
99 }
100
101 if (selector) {
102 return trySelector(selector);
103 } else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) {
104 var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__;
105 var $$testability = null;
106 try {
107 $$testability = $injector.get('$$testability');
108 } catch (e) {}
109 return {$injector: $injector, $$testability: $$testability};
110 } else {
111 return tryEl(document.body) ||
112 trySelector('[ng-app]') || trySelector('[ng\\:app]') ||
113 trySelector('[ng-controller]') || trySelector('[ng\\:controller]');
114 }
115}
116
117///////////////////////////////////////////////////////
118//// ////
119//// SCRIPTS ////
120//// ////
121///////////////////////////////////////////////////////
122
123
124/**
125 * Wait until Angular has finished rendering and has
126 * no outstanding $http calls before continuing. The specific Angular app
127 * is determined by the rootSelector.
128 *
129 * Asynchronous.
130 *
131 * @param {string} rootSelector The selector housing an ng-app
132 * @param {function(string)} callback callback. If a failure occurs, it will
133 * be passed as a parameter.
134 */
135functions.waitForAngular = function(rootSelector, callback) {
136
137 try {
138 // Wait for both angular1 testability and angular2 testability.
139
140 var testCallback = callback;
141
142 // Wait for angular1 testability first and run waitForAngular2 as a callback
143 var waitForAngular1 = function(callback) {
144
145 if (window.angular) {
146 var hooks = getNg1Hooks(rootSelector);
147 if (!hooks){
148 callback(); // not an angular1 app
149 }
150 else{
151 if (hooks.$$testability) {
152 hooks.$$testability.whenStable(callback);
153 } else if (hooks.$injector) {
154 hooks.$injector.get('$browser')
155 .notifyWhenNoOutstandingRequests(callback);
156 } else if (!rootSelector) {
157 throw new Error(
158 'Could not automatically find injector on page: "' +
159 window.location.toString() + '". Consider using config.rootEl');
160 } else {
161 throw new Error(
162 'root element (' + rootSelector + ') has no injector.' +
163 ' this may mean it is not inside ng-app.');
164 }
165 }
166 }
167 else {callback();} // not an angular1 app
168 };
169
170 // Wait for Angular2 testability and then run test callback
171 var waitForAngular2 = function() {
172 if (window.getAngularTestability) {
173 if (rootSelector) {
174 var testability = null;
175 var el = document.querySelector(rootSelector);
176 try{
177 testability = window.getAngularTestability(el);
178 }
179 catch(e){}
180 if (testability) {
181 testability.whenStable(testCallback);
182 return;
183 }
184 }
185
186 // Didn't specify root element or testability could not be found
187 // by rootSelector. This may happen in a hybrid app, which could have
188 // more than one root.
189 var testabilities = window.getAllAngularTestabilities();
190 var count = testabilities.length;
191
192 // No angular2 testability, this happens when
193 // going to a hybrid page and going back to a pure angular1 page
194 if (count === 0) {
195 testCallback();
196 return;
197 }
198
199 var decrement = function() {
200 count--;
201 if (count === 0) {
202 testCallback();
203 }
204 };
205 testabilities.forEach(function(testability) {
206 testability.whenStable(decrement);
207 });
208
209 }
210 else {testCallback();} // not an angular2 app
211 };
212
213 if (!(window.angular) && !(window.getAngularTestability)) {
214 // no testability hook
215 throw new Error(
216 'both angularJS testability and angular testability are undefined.' +
217 ' This could be either ' +
218 'because this is a non-angular page or because your test involves ' +
219 'client-side navigation, which can interfere with Protractor\'s ' +
220 'bootstrapping. See http://git.io/v4gXM for details');
221 } else {waitForAngular1(waitForAngular2);} // Wait for angular1 and angular2
222 // Testability hooks sequentially
223
224 } catch (err) {
225 callback(err.message);
226 }
227
228};
229
230/**
231 * Find a list of elements in the page by their angular binding.
232 *
233 * @param {string} binding The binding, e.g. {{cat.name}}.
234 * @param {boolean} exactMatch Whether the binding needs to be matched exactly
235 * @param {Element} using The scope of the search.
236 * @param {string} rootSelector The selector to use for the root app element.
237 *
238 * @return {Array.<Element>} The elements containing the binding.
239 */
240functions.findBindings = function(binding, exactMatch, using, rootSelector) {
241 using = using || document;
242 if (angular.getTestability) {
243 return getNg1Hooks(rootSelector).$$testability.
244 findBindings(using, binding, exactMatch);
245 }
246 var bindings = using.getElementsByClassName('ng-binding');
247 var matches = [];
248 for (var i = 0; i < bindings.length; ++i) {
249 var dataBinding = angular.element(bindings[i]).data('$binding');
250 if (dataBinding) {
251 var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding;
252 if (exactMatch) {
253 var matcher = new RegExp('({|\\s|^|\\|)' +
254 /* See http://stackoverflow.com/q/3561711 */
255 binding.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') +
256 '(}|\\s|$|\\|)');
257 if (matcher.test(bindingName)) {
258 matches.push(bindings[i]);
259 }
260 } else {
261 if (bindingName.indexOf(binding) != -1) {
262 matches.push(bindings[i]);
263 }
264 }
265
266 }
267 }
268 return matches; /* Return the whole array for webdriver.findElements. */
269};
270
271/**
272 * Find an array of elements matching a row within an ng-repeat.
273 * Always returns an array of only one element for plain old ng-repeat.
274 * Returns an array of all the elements in one segment for ng-repeat-start.
275 *
276 * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.
277 * @param {boolean} exact Whether the repeater needs to be matched exactly
278 * @param {number} index The row index.
279 * @param {Element} using The scope of the search.
280 *
281 * @return {Array.<Element>} The row of the repeater, or an array of elements
282 * in the first row in the case of ng-repeat-start.
283 */
284function findRepeaterRows(repeater, exact, index, using) {
285 using = using || document;
286
287 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
288 var rows = [];
289 for (var p = 0; p < prefixes.length; ++p) {
290 var attr = prefixes[p] + 'repeat';
291 var repeatElems = using.querySelectorAll('[' + attr + ']');
292 attr = attr.replace(/\\/g, '');
293 for (var i = 0; i < repeatElems.length; ++i) {
294 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
295 rows.push(repeatElems[i]);
296 }
297 }
298 }
299 /* multiRows is an array of arrays, where each inner array contains
300 one row of elements. */
301 var multiRows = [];
302 for (var p = 0; p < prefixes.length; ++p) {
303 var attr = prefixes[p] + 'repeat-start';
304 var repeatElems = using.querySelectorAll('[' + attr + ']');
305 attr = attr.replace(/\\/g, '');
306 for (var i = 0; i < repeatElems.length; ++i) {
307 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
308 var elem = repeatElems[i];
309 var row = [];
310 while (elem.nodeType != 8 ||
311 !repeaterMatch(elem.nodeValue, repeater)) {
312 if (elem.nodeType == 1) {
313 row.push(elem);
314 }
315 elem = elem.nextSibling;
316 }
317 multiRows.push(row);
318 }
319 }
320 }
321 var row = rows[index] || [], multiRow = multiRows[index] || [];
322 return [].concat(row, multiRow);
323}
324functions.findRepeaterRows = wrapWithHelpers(findRepeaterRows, repeaterMatch);
325
326 /**
327 * Find all rows of an ng-repeat.
328 *
329 * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.
330 * @param {boolean} exact Whether the repeater needs to be matched exactly
331 * @param {Element} using The scope of the search.
332 *
333 * @return {Array.<Element>} All rows of the repeater.
334 */
335function findAllRepeaterRows(repeater, exact, using) {
336 using = using || document;
337
338 var rows = [];
339 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
340 for (var p = 0; p < prefixes.length; ++p) {
341 var attr = prefixes[p] + 'repeat';
342 var repeatElems = using.querySelectorAll('[' + attr + ']');
343 attr = attr.replace(/\\/g, '');
344 for (var i = 0; i < repeatElems.length; ++i) {
345 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
346 rows.push(repeatElems[i]);
347 }
348 }
349 }
350 for (var p = 0; p < prefixes.length; ++p) {
351 var attr = prefixes[p] + 'repeat-start';
352 var repeatElems = using.querySelectorAll('[' + attr + ']');
353 attr = attr.replace(/\\/g, '');
354 for (var i = 0; i < repeatElems.length; ++i) {
355 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
356 var elem = repeatElems[i];
357 while (elem.nodeType != 8 ||
358 !repeaterMatch(elem.nodeValue, repeater)) {
359 if (elem.nodeType == 1) {
360 rows.push(elem);
361 }
362 elem = elem.nextSibling;
363 }
364 }
365 }
366 }
367 return rows;
368}
369functions.findAllRepeaterRows = wrapWithHelpers(findAllRepeaterRows, repeaterMatch);
370
371/**
372 * Find an element within an ng-repeat by its row and column.
373 *
374 * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.
375 * @param {boolean} exact Whether the repeater needs to be matched exactly
376 * @param {number} index The row index.
377 * @param {string} binding The column binding, e.g. '{{cat.name}}'.
378 * @param {Element} using The scope of the search.
379 * @param {string} rootSelector The selector to use for the root app element.
380 *
381 * @return {Array.<Element>} The element in an array.
382 */
383function findRepeaterElement(repeater, exact, index, binding, using, rootSelector) {
384 var matches = [];
385 using = using || document;
386
387 var rows = [];
388 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
389 for (var p = 0; p < prefixes.length; ++p) {
390 var attr = prefixes[p] + 'repeat';
391 var repeatElems = using.querySelectorAll('[' + attr + ']');
392 attr = attr.replace(/\\/g, '');
393 for (var i = 0; i < repeatElems.length; ++i) {
394 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
395 rows.push(repeatElems[i]);
396 }
397 }
398 }
399 /* multiRows is an array of arrays, where each inner array contains
400 one row of elements. */
401 var multiRows = [];
402 for (var p = 0; p < prefixes.length; ++p) {
403 var attr = prefixes[p] + 'repeat-start';
404 var repeatElems = using.querySelectorAll('[' + attr + ']');
405 attr = attr.replace(/\\/g, '');
406 for (var i = 0; i < repeatElems.length; ++i) {
407 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
408 var elem = repeatElems[i];
409 var row = [];
410 while (elem.nodeType != 8 || (elem.nodeValue &&
411 !repeaterMatch(elem.nodeValue, repeater))) {
412 if (elem.nodeType == 1) {
413 row.push(elem);
414 }
415 elem = elem.nextSibling;
416 }
417 multiRows.push(row);
418 }
419 }
420 }
421 var row = rows[index];
422 var multiRow = multiRows[index];
423 var bindings = [];
424 if (row) {
425 if (angular.getTestability) {
426 matches.push.apply(
427 matches,
428 getNg1Hooks(rootSelector).$$testability.findBindings(row, binding));
429 } else {
430 if (row.className.indexOf('ng-binding') != -1) {
431 bindings.push(row);
432 }
433 var childBindings = row.getElementsByClassName('ng-binding');
434 for (var i = 0; i < childBindings.length; ++i) {
435 bindings.push(childBindings[i]);
436 }
437 }
438 }
439 if (multiRow) {
440 for (var i = 0; i < multiRow.length; ++i) {
441 var rowElem = multiRow[i];
442 if (angular.getTestability) {
443 matches.push.apply(
444 matches,
445 getNg1Hooks(rootSelector).$$testability.findBindings(rowElem,
446 binding));
447 } else {
448 if (rowElem.className.indexOf('ng-binding') != -1) {
449 bindings.push(rowElem);
450 }
451 var childBindings = rowElem.getElementsByClassName('ng-binding');
452 for (var j = 0; j < childBindings.length; ++j) {
453 bindings.push(childBindings[j]);
454 }
455 }
456 }
457 }
458 for (var i = 0; i < bindings.length; ++i) {
459 var dataBinding = angular.element(bindings[i]).data('$binding');
460 if (dataBinding) {
461 var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding;
462 if (bindingName.indexOf(binding) != -1) {
463 matches.push(bindings[i]);
464 }
465 }
466 }
467 return matches;
468}
469functions.findRepeaterElement =
470 wrapWithHelpers(findRepeaterElement, repeaterMatch, getNg1Hooks);
471
472/**
473 * Find the elements in a column of an ng-repeat.
474 *
475 * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.
476 * @param {boolean} exact Whether the repeater needs to be matched exactly
477 * @param {string} binding The column binding, e.g. '{{cat.name}}'.
478 * @param {Element} using The scope of the search.
479 * @param {string} rootSelector The selector to use for the root app element.
480 *
481 * @return {Array.<Element>} The elements in the column.
482 */
483function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
484 var matches = [];
485 using = using || document;
486
487 var rows = [];
488 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
489 for (var p = 0; p < prefixes.length; ++p) {
490 var attr = prefixes[p] + 'repeat';
491 var repeatElems = using.querySelectorAll('[' + attr + ']');
492 attr = attr.replace(/\\/g, '');
493 for (var i = 0; i < repeatElems.length; ++i) {
494 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
495 rows.push(repeatElems[i]);
496 }
497 }
498 }
499 /* multiRows is an array of arrays, where each inner array contains
500 one row of elements. */
501 var multiRows = [];
502 for (var p = 0; p < prefixes.length; ++p) {
503 var attr = prefixes[p] + 'repeat-start';
504 var repeatElems = using.querySelectorAll('[' + attr + ']');
505 attr = attr.replace(/\\/g, '');
506 for (var i = 0; i < repeatElems.length; ++i) {
507 if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) {
508 var elem = repeatElems[i];
509 var row = [];
510 while (elem.nodeType != 8 || (elem.nodeValue &&
511 !repeaterMatch(elem.nodeValue, repeater))) {
512 if (elem.nodeType == 1) {
513 row.push(elem);
514 }
515 elem = elem.nextSibling;
516 }
517 multiRows.push(row);
518 }
519 }
520 }
521 var bindings = [];
522 for (var i = 0; i < rows.length; ++i) {
523 if (angular.getTestability) {
524 matches.push.apply(
525 matches,
526 getNg1Hooks(rootSelector).$$testability.findBindings(rows[i],
527 binding));
528 } else {
529 if (rows[i].className.indexOf('ng-binding') != -1) {
530 bindings.push(rows[i]);
531 }
532 var childBindings = rows[i].getElementsByClassName('ng-binding');
533 for (var k = 0; k < childBindings.length; ++k) {
534 bindings.push(childBindings[k]);
535 }
536 }
537 }
538 for (var i = 0; i < multiRows.length; ++i) {
539 for (var j = 0; j < multiRows[i].length; ++j) {
540 if (angular.getTestability) {
541 matches.push.apply(
542 matches,
543 getNg1Hooks(rootSelector).$$testability.findBindings(
544 multiRows[i][j], binding));
545 } else {
546 var elem = multiRows[i][j];
547 if (elem.className.indexOf('ng-binding') != -1) {
548 bindings.push(elem);
549 }
550 var childBindings = elem.getElementsByClassName('ng-binding');
551 for (var k = 0; k < childBindings.length; ++k) {
552 bindings.push(childBindings[k]);
553 }
554 }
555 }
556 }
557 for (var j = 0; j < bindings.length; ++j) {
558 var dataBinding = angular.element(bindings[j]).data('$binding');
559 if (dataBinding) {
560 var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding;
561 if (bindingName.indexOf(binding) != -1) {
562 matches.push(bindings[j]);
563 }
564 }
565 }
566 return matches;
567}
568functions.findRepeaterColumn =
569 wrapWithHelpers(findRepeaterColumn, repeaterMatch, getNg1Hooks);
570
571/**
572 * Find elements by model name.
573 *
574 * @param {string} model The model name.
575 * @param {Element} using The scope of the search.
576 * @param {string} rootSelector The selector to use for the root app element.
577 *
578 * @return {Array.<Element>} The matching elements.
579 */
580functions.findByModel = function(model, using, rootSelector) {
581 using = using || document;
582
583 if (angular.getTestability) {
584 return getNg1Hooks(rootSelector).$$testability.
585 findModels(using, model, true);
586 }
587 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
588 for (var p = 0; p < prefixes.length; ++p) {
589 var selector = '[' + prefixes[p] + 'model="' + model + '"]';
590 var elements = using.querySelectorAll(selector);
591 if (elements.length) {
592 return elements;
593 }
594 }
595};
596
597/**
598 * Find elements by options.
599 *
600 * @param {string} optionsDescriptor The descriptor for the option
601 * (i.e. fruit for fruit in fruits).
602 * @param {Element} using The scope of the search.
603 *
604 * @return {Array.<Element>} The matching elements.
605 */
606functions.findByOptions = function(optionsDescriptor, using) {
607 using = using || document;
608
609 var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
610 for (var p = 0; p < prefixes.length; ++p) {
611 var selector = '[' + prefixes[p] + 'options="' + optionsDescriptor + '"] option';
612 var elements = using.querySelectorAll(selector);
613 if (elements.length) {
614 return elements;
615 }
616 }
617};
618
619/**
620 * Find buttons by textual content.
621 *
622 * @param {string} searchText The exact text to match.
623 * @param {Element} using The scope of the search.
624 *
625 * @return {Array.<Element>} The matching elements.
626 */
627functions.findByButtonText = function(searchText, using) {
628 using = using || document;
629
630 var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]');
631 var matches = [];
632 for (var i = 0; i < elements.length; ++i) {
633 var element = elements[i];
634 var elementText;
635 if (element.tagName.toLowerCase() == 'button') {
636 elementText = element.textContent || element.innerText || '';
637 } else {
638 elementText = element.value;
639 }
640 if (elementText.trim() === searchText) {
641 matches.push(element);
642 }
643 }
644
645 return matches;
646};
647
648/**
649 * Find buttons by textual content.
650 *
651 * @param {string} searchText The exact text to match.
652 * @param {Element} using The scope of the search.
653 *
654 * @return {Array.<Element>} The matching elements.
655 */
656functions.findByPartialButtonText = function(searchText, using) {
657 using = using || document;
658
659 var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]');
660 var matches = [];
661 for (var i = 0; i < elements.length; ++i) {
662 var element = elements[i];
663 var elementText;
664 if (element.tagName.toLowerCase() == 'button') {
665 elementText = element.textContent || element.innerText || '';
666 } else {
667 elementText = element.value;
668 }
669 if (elementText.indexOf(searchText) > -1) {
670 matches.push(element);
671 }
672 }
673
674 return matches;
675};
676
677/**
678 * Find elements by css selector and textual content.
679 *
680 * @param {string} cssSelector The css selector to match.
681 * @param {string} searchText The exact text to match or a serialized regex.
682 * @param {Element} using The scope of the search.
683 *
684 * @return {Array.<Element>} An array of matching elements.
685 */
686functions.findByCssContainingText = function(cssSelector, searchText, using) {
687 using = using || document;
688
689 if (searchText.indexOf('__REGEXP__') === 0) {
690 var match = searchText.split('__REGEXP__')[1].match(/\/(.*)\/(.*)?/);
691 searchText = new RegExp(match[1], match[2] || '');
692 }
693 var elements = using.querySelectorAll(cssSelector);
694 var matches = [];
695 for (var i = 0; i < elements.length; ++i) {
696 var element = elements[i];
697 var elementText = element.textContent || element.innerText || '';
698 var elementMatches = searchText instanceof RegExp ?
699 searchText.test(elementText) :
700 elementText.indexOf(searchText) > -1;
701
702 if (elementMatches) {
703 matches.push(element);
704 }
705 }
706 return matches;
707};
708
709/**
710 * Tests whether the angular global variable is present on a page. Retries
711 * in case the page is just loading slowly.
712 *
713 * Asynchronous.
714 *
715 * @param {number} attempts Number of times to retry.
716 * @param {boolean} ng12Hybrid Flag set if app is a hybrid of angular 1 and 2
717 * @param {function({version: ?number, message: ?string})} asyncCallback callback
718 *
719 */
720functions.testForAngular = function(attempts, ng12Hybrid, asyncCallback) {
721 var callback = function(args) {
722 setTimeout(function() {
723 asyncCallback(args);
724 }, 0);
725 };
726 var definitelyNg1 = !!ng12Hybrid;
727 var definitelyNg2OrNewer = false;
728 var check = function(n) {
729 try {
730 /* Figure out which version of angular we're waiting on */
731 if (!definitelyNg1 && !definitelyNg2OrNewer) {
732 if (window.angular && !(window.angular.version && window.angular.version.major > 1)) {
733 definitelyNg1 = true;
734 } else if (window.getAllAngularTestabilities) {
735 definitelyNg2OrNewer = true;
736 }
737 }
738 /* See if our version of angular is ready */
739 if (definitelyNg1) {
740 if (window.angular && window.angular.resumeBootstrap) {
741 return callback({ver: 1});
742 }
743 } else if (definitelyNg2OrNewer) {
744 if (true /* ng2 has no resumeBootstrap() */) {
745 return callback({ver: 2});
746 }
747 }
748 /* Try again (or fail) */
749 if (n < 1) {
750 if (definitelyNg1 && window.angular) {
751 callback({message: 'angular never provided resumeBootstrap'});
752 } else if (ng12Hybrid && !window.angular) {
753 callback({message: 'angular 1 never loaded' +
754 window.getAllAngularTestabilities ? ' (are you sure this app ' +
755 'uses ngUpgrade? Try un-setting ng12Hybrid)' : ''});
756 } else {
757 callback({message: 'retries looking for angular exceeded'});
758 }
759 } else {
760 window.setTimeout(function() {check(n - 1);}, 1000);
761 }
762 } catch (e) {
763 callback({message: e});
764 }
765 };
766 check(attempts);
767};
768
769/**
770 * Evalute an Angular expression in the context of a given element.
771 *
772 * @param {Element} element The element in whose scope to evaluate.
773 * @param {string} expression The expression to evaluate.
774 *
775 * @return {?Object} The result of the evaluation.
776 */
777functions.evaluate = function(element, expression) {
778 return angular.element(element).scope().$eval(expression);
779};
780
781functions.allowAnimations = function(element, value) {
782 var ngElement = angular.element(element);
783 if (ngElement.allowAnimations) {
784 // AngularDart: $testability API.
785 return ngElement.allowAnimations(value);
786 } else {
787 // AngularJS
788 var enabledFn = ngElement.injector().get('$animate').enabled;
789 return (value == null) ? enabledFn() : enabledFn(value);
790 }
791};
792
793/**
794 * Return the current url using $location.absUrl().
795 *
796 * @param {string} selector The selector housing an ng-app
797 */
798functions.getLocationAbsUrl = function(selector) {
799 var hooks = getNg1Hooks(selector);
800 if (angular.getTestability) {
801 return hooks.$$testability.getLocation();
802 }
803 return hooks.$injector.get('$location').absUrl();
804};
805
806/**
807 * Browse to another page using in-page navigation.
808 *
809 * @param {string} selector The selector housing an ng-app
810 * @param {string} url In page URL using the same syntax as $location.url(),
811 * /path?search=a&b=c#hash
812 */
813functions.setLocation = function(selector, url) {
814 var hooks = getNg1Hooks(selector);
815 if (angular.getTestability) {
816 return hooks.$$testability.setLocation(url);
817 }
818 var $injector = hooks.$injector;
819 var $location = $injector.get('$location');
820 var $rootScope = $injector.get('$rootScope');
821
822 if (url !== $location.url()) {
823 $location.url(url);
824 $rootScope.$digest();
825 }
826};
827
828/**
829 * Retrieve the pending $http requests.
830 *
831 * @param {string} selector The selector housing an ng-app
832 * @return {!Array<!Object>} An array of pending http requests.
833 */
834functions.getPendingHttpRequests = function(selector) {
835 var hooks = getNg1Hooks(selector, true);
836 var $http = hooks.$injector.get('$http');
837 return $http.pendingRequests;
838};
839
840['waitForAngular', 'findBindings', 'findByModel', 'getLocationAbsUrl',
841 'setLocation', 'getPendingHttpRequests'].forEach(function(funName) {
842 functions[funName] = wrapWithHelpers(functions[funName], getNg1Hooks);
843});
844
845/* Publish all the functions as strings to pass to WebDriver's
846 * exec[Async]Script. In addition, also include a script that will
847 * install all the functions on window (for debugging.)
848 *
849 * We also wrap any exceptions thrown by a clientSideScripts function
850 * that is not an instance of the Error type into an Error type. If we
851 * don't do so, then the resulting stack trace is completely unhelpful
852 * and the exception message is just "unknown error." These types of
853 * exceptions are the common case for dart2js code. This wrapping gives
854 * us the Dart stack trace and exception message.
855 */
856var util = require('util');
857var scriptsList = [];
858var scriptFmt = (
859 'try { return (%s).apply(this, arguments); }\n' +
860 'catch(e) { throw (e instanceof Error) ? e : new Error(e); }');
861for (var fnName in functions) {
862 if (functions.hasOwnProperty(fnName)) {
863 exports[fnName] = util.format(scriptFmt, functions[fnName]);
864 scriptsList.push(util.format('%s: %s', fnName, functions[fnName]));
865 }
866}
867
868exports.installInBrowser = (util.format(
869 'window.clientSideScripts = {%s};', scriptsList.join(', ')));
870
871/**
872 * Automatically installed by Protractor when a page is loaded, this
873 * default mock module decorates $timeout to keep track of any
874 * outstanding timeouts.
875 *
876 * @param {boolean} trackOutstandingTimeouts
877 */
878exports.protractorBaseModuleFn = function(trackOutstandingTimeouts) {
879 var ngMod = angular.module('protractorBaseModule_', []).config([
880 '$compileProvider',
881 function($compileProvider) {
882 if ($compileProvider.debugInfoEnabled) {
883 $compileProvider.debugInfoEnabled(true);
884 }
885 }
886 ]);
887 if (trackOutstandingTimeouts) {
888 ngMod.config([
889 '$provide',
890 function ($provide) {
891 $provide.decorator('$timeout', [
892 '$delegate',
893 function ($delegate) {
894 var $timeout = $delegate;
895
896 var taskId = 0;
897
898 if (!window['NG_PENDING_TIMEOUTS']) {
899 window['NG_PENDING_TIMEOUTS'] = {};
900 }
901
902 var extendedTimeout= function() {
903 var args = Array.prototype.slice.call(arguments);
904 if (typeof(args[0]) !== 'function') {
905 return $timeout.apply(null, args);
906 }
907
908 taskId++;
909 var fn = args[0];
910 window['NG_PENDING_TIMEOUTS'][taskId] =
911 fn.toString();
912 var wrappedFn = (function(taskId_) {
913 return function() {
914 delete window['NG_PENDING_TIMEOUTS'][taskId_];
915 return fn.apply(null, arguments);
916 };
917 })(taskId);
918 args[0] = wrappedFn;
919
920 var promise = $timeout.apply(null, args);
921 promise.ptorTaskId_ = taskId;
922 return promise;
923 };
924
925 extendedTimeout.cancel = function() {
926 var taskId_ = arguments[0] && arguments[0].ptorTaskId_;
927 if (taskId_) {
928 delete window['NG_PENDING_TIMEOUTS'][taskId_];
929 }
930 return $timeout.cancel.apply($timeout, arguments);
931 };
932
933 return extendedTimeout;
934 }
935 ]);
936 }
937 ]);
938 }
939};