clientsidescripts.js 31 KB

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