command-wrapper.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. module.exports = new (function() {
  2. /**
  3. * Given an element name, returns that element object
  4. *
  5. * @param {Object} parent The parent page or section
  6. * @param {string} elementName Name of element
  7. * @returns {Object} The element object
  8. */
  9. function getElement(parent, elementName) {
  10. elementName = elementName.substring(1);
  11. if (!(elementName in parent.elements)) {
  12. throw new Error(elementName + ' was not found in "' + parent.name +
  13. '". Available elements: ' + Object.keys(parent.elements));
  14. }
  15. return parent.elements[elementName];
  16. }
  17. /**
  18. * Given a section name, returns that section object
  19. *
  20. * @param {Object} parent The parent page or section
  21. * @param {string} sectionName Name of section
  22. * @returns {Object} The section object
  23. */
  24. function getSection(parent, sectionName) {
  25. sectionName = sectionName.substring(1);
  26. if (!(sectionName in parent.section)) {
  27. throw new Error(sectionName + ' was not found in "' + parent.name +
  28. '". Available sections: ' + Object.keys(parent.sections));
  29. }
  30. return parent.section[sectionName];
  31. }
  32. /**
  33. * Calls use(Css|Xpath|Recursion) command
  34. *
  35. * Uses `useXpath`, `useCss`, and `useRecursion` commands.
  36. *
  37. * @param {Object} client The Nightwatch instance
  38. * @param {string} desiredStrategy (css selector|xpath|recursion)
  39. * @returns {null}
  40. */
  41. function setLocateStrategy(client, desiredStrategy) {
  42. var methodMap = {
  43. xpath : 'useXpath',
  44. 'css selector' : 'useCss',
  45. recursion : 'useRecursion'
  46. };
  47. if (desiredStrategy in methodMap) {
  48. client.api[methodMap[desiredStrategy]]();
  49. }
  50. }
  51. /**
  52. * Creates a closure that enables calling commands and assertions on the page or section.
  53. * For all element commands and assertions, it fetches element's selector and locate strategy
  54. * For elements nested under sections, it sets 'recursion' as the locate strategy and passes as its first argument to the command an array of its ancestors + self
  55. * If the command or assertion is not on an element, it calls it with the untouched passed arguments
  56. *
  57. * @param {Object} parent The parent page or section
  58. * @param {function} commandFn The actual command function
  59. * @param {string} commandName The name of the command ("click", "containsText", etc)
  60. * @param {Boolean} [isChaiAssertion]
  61. * @returns {function}
  62. */
  63. function makeWrappedCommand(parent, commandFn, commandName, isChaiAssertion) {
  64. return function() {
  65. var args = Array.prototype.slice.call(arguments);
  66. var prevLocateStrategy = parent.client.locateStrategy;
  67. var elementCommand = isElementCommand(args);
  68. if (elementCommand) {
  69. var firstArg;
  70. var desiredStrategy;
  71. var callbackIndex;
  72. var originalCallback;
  73. var elementOrSectionName = args.shift();
  74. var getter = (isChaiAssertion && commandName === 'section') ? getSection : getElement;
  75. var elementOrSection = getter(parent, elementOrSectionName);
  76. var ancestors = getAncestorsWithElement(elementOrSection);
  77. if (ancestors.length === 1) {
  78. firstArg = elementOrSection.selector;
  79. desiredStrategy = elementOrSection.locateStrategy;
  80. } else {
  81. firstArg = ancestors;
  82. desiredStrategy = 'recursion';
  83. }
  84. setLocateStrategy(parent.client, desiredStrategy);
  85. args.unshift(firstArg);
  86. // if a callback is being used with this command, wrap it in
  87. // a function that allows us to restore the locate strategy
  88. // to its original value before the callback is called
  89. callbackIndex = findCallbackIndex(args);
  90. if (callbackIndex !== -1) {
  91. originalCallback = args[callbackIndex];
  92. args[callbackIndex] = function callbackWrapper() {
  93. // restore the locate strategy directly through client.locateStrategy.
  94. // setLocateStrategy() can't be used since it uses the api commands
  95. // which get added to the command queue and will not update the
  96. // strategy in time for the callback which is getting immediately
  97. // called after
  98. parent.client.locateStrategy = prevLocateStrategy;
  99. return originalCallback.apply(parent.client.api, arguments);
  100. };
  101. }
  102. }
  103. var c = commandFn.apply(parent.client, args);
  104. if (elementCommand) {
  105. setLocateStrategy(parent.client, prevLocateStrategy);
  106. }
  107. return (isChaiAssertion ? c : parent);
  108. };
  109. }
  110. /**
  111. *
  112. * @param {Array} args
  113. * @return {boolean}
  114. */
  115. function isElementCommand(args) {
  116. return (args.length > 0) && (args[0].toString().indexOf('@') === 0);
  117. }
  118. /**
  119. * Identifies the location of a callback function within an arguments array.
  120. *
  121. * @param {Array} args Arguments array in which to find the location of a callback.
  122. * @returns {number} Index location of the callback in the args array. If not found, -1 is returned.
  123. */
  124. function findCallbackIndex(args) {
  125. if (args.length === 0) {
  126. return -1;
  127. }
  128. // callbacks will usually be the last argument. waitFor methods allow an additional
  129. // message argument to follow the callback which will also need to be checked for.
  130. // last argument
  131. var index = args.length - 1;
  132. if (typeof args[index] === 'function') {
  133. return index;
  134. }
  135. // second to last argument (waitfor calls)
  136. index--;
  137. if (typeof args[index] === 'function') {
  138. return index;
  139. }
  140. return -1;
  141. }
  142. /**
  143. * Retrieves an array of ancestors of the supplied element. The last element in the array is the element object itself
  144. *
  145. * @param {Object} element The element
  146. * @returns {Array}
  147. */
  148. function getAncestorsWithElement(element) {
  149. var elements = [];
  150. function addElement(e) {
  151. elements.unshift(e);
  152. if (e.parent && e.parent.selector) {
  153. addElement(e.parent);
  154. }
  155. }
  156. addElement(element);
  157. return elements;
  158. }
  159. /**
  160. * Adds commands (elements commands, assertions, etc) to the page or section
  161. *
  162. * @param {Object} parent The parent page or section
  163. * @param {Object} target What the command is added to (parent|section or assertion object on parent|section)
  164. * @param {Object} commands
  165. * @returns {null}
  166. */
  167. function applyCommandsToTarget(parent, target, commands) {
  168. Object.keys(commands).forEach(function(commandName) {
  169. if (isValidAssertion(commandName)) {
  170. target[commandName] = target[commandName] || {};
  171. var isChaiAssertion = commandName === 'expect';
  172. var assertions = commands[commandName];
  173. Object.keys(assertions).forEach(function(assertionName) {
  174. target[commandName][assertionName] = addCommand(target[commandName], assertions[assertionName], assertionName, parent, isChaiAssertion);
  175. });
  176. } else {
  177. target[commandName] = addCommand(target, commands[commandName], commandName, parent, false);
  178. }
  179. });
  180. }
  181. function addCommand(target, commandFn, commandName, parent, isChaiAssertion) {
  182. if (target[commandName]) {
  183. parent.client.results.errors++;
  184. var error = new Error('The command "' + commandName + '" is already defined!');
  185. parent.client.errors.push(error.stack);
  186. throw error;
  187. }
  188. return makeWrappedCommand(parent, commandFn, commandName, isChaiAssertion);
  189. }
  190. function isValidAssertion(commandName) {
  191. return ['assert', 'verify', 'expect'].indexOf(commandName) > -1;
  192. }
  193. /**
  194. * Entrypoint to add commands (elements commands, assertions, etc) to the page or section
  195. *
  196. * @param {Object} parent The parent page or section
  197. * @param {function} commandLoader function that retrieves commands
  198. * @returns {null}
  199. */
  200. this.addWrappedCommands = function (parent, commandLoader) {
  201. var commands = {};
  202. commands = commandLoader(commands);
  203. applyCommandsToTarget(parent, parent, commands);
  204. };
  205. })();