index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /**
  2. * Adapts Jasmine-Node tests to work better with WebDriverJS. Borrows
  3. * heavily from the mocha WebDriverJS adapter at
  4. * https://code.google.com/p/selenium/source/browse/javascript/node/selenium-webdriver/testing/index.js
  5. */
  6. var WebElement; // Equal to webdriver.WebElement
  7. var idleEventName = 'idle'; // Equal to webdriver.promise.ControlFlow.EventType.IDLE
  8. var maybePromise = require('./maybePromise');
  9. /**
  10. * Validates that the parameter is a function.
  11. * @param {Object} functionToValidate The function to validate.
  12. * @throws {Error}
  13. * @return {Object} The original parameter.
  14. */
  15. function validateFunction(functionToValidate) {
  16. if (functionToValidate && typeof functionToValidate === 'function') {
  17. return functionToValidate;
  18. } else {
  19. throw Error(functionToValidate + ' is not a function');
  20. }
  21. }
  22. /**
  23. * Validates that the parameter is a number.
  24. * @param {Object} numberToValidate The number to validate.
  25. * @throws {Error}
  26. * @return {Object} The original number.
  27. */
  28. function validateNumber(numberToValidate) {
  29. if (!isNaN(numberToValidate)) {
  30. return numberToValidate;
  31. } else {
  32. throw Error(numberToValidate + ' is not a number');
  33. }
  34. }
  35. /**
  36. * Validates that the parameter is a string.
  37. * @param {Object} stringToValidate The string to validate.
  38. * @throws {Error}
  39. * @return {Object} The original string.
  40. */
  41. function validateString(stringtoValidate) {
  42. if (typeof stringtoValidate == 'string' || stringtoValidate instanceof String) {
  43. return stringtoValidate;
  44. } else {
  45. throw Error(stringtoValidate + ' is not a string');
  46. }
  47. }
  48. /**
  49. * Calls a function once the scheduler is idle. If the scheduler does not support the idle API,
  50. * calls the function immediately. See scheduler.md#idle-api for details.
  51. *
  52. * @param {Object} scheduler The scheduler to wait for.
  53. * @param {!Function} fn The function to call.
  54. */
  55. function callWhenIdle(scheduler, fn) {
  56. if (!scheduler.once || !scheduler.isIdle || scheduler.isIdle()) {
  57. fn();
  58. } else {
  59. scheduler.once(idleEventName, function() { fn(); });
  60. }
  61. }
  62. /**
  63. * Wraps a function so it runs inside a scheduler's `execute()` block.
  64. *
  65. * In the most common case, this means wrapping in a `webdriver.promise.ControlFlow` instance
  66. * to wait for the control flow to complete one task before starting the next. See scheduler.md
  67. * for details.
  68. *
  69. * @param {!Object} scheduler See scheduler.md for details.
  70. * @param {!Function} newPromise Makes a new promise using whatever implementation the scheduler
  71. * prefers.
  72. * @param {!Function} globalFn The function to wrap.
  73. * @param {!string} fnName The name of the function being wrapped (e.g. `'it'`).
  74. * @return {!Function} The new function.
  75. */
  76. function wrapInScheduler(scheduler, newPromise, globalFn, fnName) {
  77. return function() {
  78. var driverError = new Error();
  79. driverError.stack = driverError.stack.replace(/ +at.+jasminewd.+\n/, '');
  80. function asyncTestFn(fn, description) {
  81. description = description ? ('("' + description + '")') : '';
  82. return function(done) {
  83. var async = fn.length > 0;
  84. var testFn = fn.bind(this);
  85. scheduler.execute(function schedulerExecute() {
  86. return newPromise(function(fulfill, reject) {
  87. function wrappedReject(err) {
  88. if(err instanceof Error)
  89. reject(err);
  90. else
  91. reject(new Error(err));
  92. }
  93. if (async) {
  94. // If testFn is async (it expects a done callback), resolve the promise of this
  95. // test whenever that callback says to. Any promises returned from testFn are
  96. // ignored.
  97. var proxyDone = fulfill;
  98. proxyDone.fail = wrappedReject;
  99. testFn(proxyDone);
  100. } else {
  101. // Without a callback, testFn can return a promise, or it will
  102. // be assumed to have completed synchronously.
  103. var ret = testFn();
  104. if (maybePromise.isPromise(ret)) {
  105. ret.then(fulfill, wrappedReject);
  106. } else {
  107. fulfill(ret);
  108. }
  109. }
  110. });
  111. }, 'Run ' + fnName + description + ' in control flow').then(
  112. callWhenIdle.bind(null, scheduler, done), function(err) {
  113. if (!err) {
  114. err = new Error('Unknown Error');
  115. err.stack = '';
  116. }
  117. err.stack = err.stack + '\nFrom asynchronous test: \n' + driverError.stack;
  118. callWhenIdle(scheduler, done.fail.bind(done, err));
  119. }
  120. );
  121. };
  122. }
  123. var description, func, timeout;
  124. switch (fnName) {
  125. case 'it':
  126. case 'fit':
  127. description = validateString(arguments[0]);
  128. if (!arguments[1]) {
  129. return globalFn(description);
  130. }
  131. func = validateFunction(arguments[1]);
  132. if (!arguments[2]) {
  133. return globalFn(description, asyncTestFn(func, description));
  134. } else {
  135. timeout = validateNumber(arguments[2]);
  136. return globalFn(description, asyncTestFn(func, description), timeout);
  137. }
  138. break;
  139. case 'beforeEach':
  140. case 'afterEach':
  141. case 'beforeAll':
  142. case 'afterAll':
  143. func = validateFunction(arguments[0]);
  144. if (!arguments[1]) {
  145. globalFn(asyncTestFn(func));
  146. } else {
  147. timeout = validateNumber(arguments[1]);
  148. globalFn(asyncTestFn(func), timeout);
  149. }
  150. break;
  151. default:
  152. throw Error('invalid function: ' + fnName);
  153. }
  154. };
  155. }
  156. /**
  157. * Initialize the JasmineWd adapter with a particlar scheduler, generally a webdriver control flow.
  158. *
  159. * @param {Object=} scheduler The scheduler to wrap tests in. See scheduler.md for details.
  160. * Defaults to a mock scheduler that calls functions immediately.
  161. * @param {Object=} webdriver The result of `require('selenium-webdriver')`. Passed in here rather
  162. * than required by jasminewd directly so that jasminewd can't end up up with a different version
  163. * of `selenium-webdriver` than your tests use. If not specified, jasminewd will still work, but
  164. * it won't check for `WebElement` instances in expect() statements and could cause control flow
  165. * problems if your tests are using an old version of `selenium-webdriver` (e.g. version 2.53.0).
  166. */
  167. function initJasmineWd(scheduler, webdriver) {
  168. if (jasmine.JasmineWdInitialized) {
  169. throw Error('JasmineWd already initialized when init() was called');
  170. }
  171. jasmine.JasmineWdInitialized = true;
  172. // Pull information from webdriver instance
  173. if (webdriver) {
  174. WebElement = webdriver.WebElement || WebElement;
  175. idleEventName = (
  176. webdriver.promise &&
  177. webdriver.promise.ControlFlow &&
  178. webdriver.promise.ControlFlow.EventType &&
  179. webdriver.promise.ControlFlow.EventType.IDLE
  180. ) || idleEventname;
  181. }
  182. // Default to mock scheduler
  183. if (!scheduler) {
  184. scheduler = { execute: function(fn) {
  185. return Promise.resolve().then(fn);
  186. } };
  187. }
  188. // Figure out how we're getting new promises
  189. var newPromise;
  190. if (typeof scheduler.promise == 'function') {
  191. newPromise = scheduler.promise.bind(scheduler);
  192. } else if (webdriver && webdriver.promise && webdriver.promise.ControlFlow &&
  193. (scheduler instanceof webdriver.promise.ControlFlow) &&
  194. (webdriver.promise.USE_PROMISE_MANAGER !== false)) {
  195. newPromise = function(resolver) {
  196. return new webdriver.promise.Promise(resolver, scheduler);
  197. };
  198. } else {
  199. newPromise = function(resolver) {
  200. return new Promise(resolver);
  201. };
  202. }
  203. // Wrap functions
  204. global.it = wrapInScheduler(scheduler, newPromise, global.it, 'it');
  205. global.fit = wrapInScheduler(scheduler, newPromise, global.fit, 'fit');
  206. global.beforeEach = wrapInScheduler(scheduler, newPromise, global.beforeEach, 'beforeEach');
  207. global.afterEach = wrapInScheduler(scheduler, newPromise, global.afterEach, 'afterEach');
  208. global.beforeAll = wrapInScheduler(scheduler, newPromise, global.beforeAll, 'beforeAll');
  209. global.afterAll = wrapInScheduler(scheduler, newPromise, global.afterAll, 'afterAll');
  210. // Reset API
  211. if (scheduler.reset) {
  212. // On timeout, the flow should be reset. This will prevent webdriver tasks
  213. // from overflowing into the next test and causing it to fail or timeout
  214. // as well. This is done in the reporter instead of an afterEach block
  215. // to ensure that it runs after any afterEach() blocks with webdriver tasks
  216. // get to complete first.
  217. jasmine.getEnv().addReporter(new OnTimeoutReporter(function() {
  218. console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.');
  219. scheduler.reset();
  220. }));
  221. }
  222. }
  223. var originalExpect = global.expect;
  224. global.expect = function(actual) {
  225. if (WebElement && (actual instanceof WebElement)) {
  226. throw Error('expect called with WebElement argument, expected a Promise. ' +
  227. 'Did you mean to use .getText()?');
  228. }
  229. return originalExpect(actual);
  230. };
  231. /**
  232. * Creates a matcher wrapper that resolves any promises given for actual and
  233. * expected values, as well as the `pass` property of the result.
  234. *
  235. * Wrapped matchers will return either `undefined` or a promise which resolves
  236. * when the matcher is complete, depending on if the matcher had to resolve any
  237. * promises.
  238. */
  239. jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
  240. return function() {
  241. var expected = Array.prototype.slice.call(arguments, 0),
  242. expectation = this,
  243. matchError = new Error("Failed expectation");
  244. matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, '');
  245. // Return either undefined or a promise of undefined
  246. return maybePromise(expectation.actual, function(actual) {
  247. return maybePromise.all(expected, function(expected) {
  248. return compare(actual, expected);
  249. });
  250. });
  251. function compare(actual, expected) {
  252. var args = expected.slice(0);
  253. args.unshift(actual);
  254. var matcher = matcherFactory(expectation.util, expectation.customEqualityTesters);
  255. var matcherCompare = matcher.compare;
  256. if (expectation.isNot) {
  257. matcherCompare = matcher.negativeCompare || defaultNegativeCompare;
  258. }
  259. var result = matcherCompare.apply(null, args);
  260. return maybePromise(result.pass, compareDone);
  261. // compareDone always returns undefined
  262. function compareDone(pass) {
  263. var message = '';
  264. if (!pass) {
  265. if (!result.message) {
  266. args.unshift(expectation.isNot);
  267. args.unshift(name);
  268. message = expectation.util.buildFailureMessage.apply(null, args);
  269. } else {
  270. if (Object.prototype.toString.apply(result.message) === '[object Function]') {
  271. message = result.message(expectation.isNot);
  272. } else {
  273. message = result.message;
  274. }
  275. }
  276. }
  277. if (expected.length == 1) {
  278. expected = expected[0];
  279. }
  280. var res = {
  281. matcherName: name,
  282. passed: pass,
  283. message: message,
  284. actual: actual,
  285. expected: expected,
  286. error: matchError
  287. };
  288. expectation.addExpectationResult(pass, res);
  289. }
  290. function defaultNegativeCompare() {
  291. var result = matcher.compare.apply(null, args);
  292. result.pass = maybePromise(result.pass, function(pass) {
  293. return !pass;
  294. });
  295. return result;
  296. }
  297. }
  298. };
  299. };
  300. // Re-add core matchers so they are wrapped.
  301. jasmine.Expectation.addCoreMatchers(jasmine.matchers);
  302. /**
  303. * A Jasmine reporter which does nothing but execute the input function
  304. * on a timeout failure.
  305. */
  306. var OnTimeoutReporter = function(fn) {
  307. this.callback = fn;
  308. };
  309. OnTimeoutReporter.prototype.specDone = function(result) {
  310. if (result.status === 'failed') {
  311. for (var i = 0; i < result.failedExpectations.length; i++) {
  312. var failureMessage = result.failedExpectations[i].message;
  313. if (failureMessage.match(/Timeout/)) {
  314. this.callback();
  315. }
  316. }
  317. }
  318. };
  319. module.exports.init = initJasmineWd;