test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. 'use strict';
  2. const isGeneratorFn = require('is-generator-fn');
  3. const co = require('co-with-promise');
  4. const concordance = require('concordance');
  5. const observableToPromise = require('observable-to-promise');
  6. const isPromise = require('is-promise');
  7. const isObservable = require('is-observable');
  8. const plur = require('plur');
  9. const assert = require('./assert');
  10. const nowAndTimers = require('./now-and-timers');
  11. const concordanceOptions = require('./concordance-options').default;
  12. function formatErrorValue(label, error) {
  13. const formatted = concordance.format(error, concordanceOptions);
  14. return {label, formatted};
  15. }
  16. const captureStack = start => {
  17. const limitBefore = Error.stackTraceLimit;
  18. Error.stackTraceLimit = 1;
  19. const obj = {};
  20. Error.captureStackTrace(obj, start);
  21. Error.stackTraceLimit = limitBefore;
  22. return obj.stack;
  23. };
  24. const assertions = assert.wrapAssertions({
  25. pass(test) {
  26. test.countPassedAssertion();
  27. },
  28. pending(test, promise) {
  29. test.addPendingAssertion(promise);
  30. },
  31. fail(test, error) {
  32. test.addFailedAssertion(error);
  33. }
  34. });
  35. const assertionNames = Object.keys(assertions);
  36. function log() {
  37. const args = Array.from(arguments, value => {
  38. return typeof value === 'string' ?
  39. value :
  40. concordance.format(value, concordanceOptions);
  41. });
  42. if (args.length > 0) {
  43. this.addLog(args.join(' '));
  44. }
  45. }
  46. function plan(count) {
  47. this.plan(count, captureStack(this.plan));
  48. }
  49. const testMap = new WeakMap();
  50. class ExecutionContext {
  51. constructor(test) {
  52. testMap.set(this, test);
  53. const skip = () => {
  54. test.countPassedAssertion();
  55. };
  56. const boundPlan = plan.bind(test);
  57. boundPlan.skip = () => {};
  58. Object.defineProperties(this, assertionNames.reduce((props, name) => {
  59. props[name] = {value: assertions[name].bind(test)};
  60. props[name].value.skip = skip;
  61. return props;
  62. }, {
  63. log: {value: log.bind(test)},
  64. plan: {value: boundPlan}
  65. }));
  66. this.snapshot.skip = () => {
  67. test.skipSnapshot();
  68. };
  69. }
  70. get end() {
  71. const end = testMap.get(this).bindEndCallback();
  72. const endFn = err => end(err, captureStack(endFn));
  73. return endFn;
  74. }
  75. get title() {
  76. return testMap.get(this).title;
  77. }
  78. get context() {
  79. return testMap.get(this).contextRef.get();
  80. }
  81. set context(context) {
  82. testMap.get(this).contextRef.set(context);
  83. }
  84. _throwsArgStart(assertion, file, line) {
  85. testMap.get(this).trackThrows({assertion, file, line});
  86. }
  87. _throwsArgEnd() {
  88. testMap.get(this).trackThrows(null);
  89. }
  90. }
  91. class Test {
  92. constructor(options) {
  93. this.contextRef = options.contextRef;
  94. this.failWithoutAssertions = options.failWithoutAssertions;
  95. this.fn = isGeneratorFn(options.fn) ? co.wrap(options.fn) : options.fn;
  96. this.metadata = options.metadata;
  97. this.title = options.title;
  98. this.logs = [];
  99. this.snapshotInvocationCount = 0;
  100. this.compareWithSnapshot = assertionOptions => {
  101. const belongsTo = assertionOptions.id || this.title;
  102. const expected = assertionOptions.expected;
  103. const index = assertionOptions.id ? 0 : this.snapshotInvocationCount++;
  104. const label = assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}`;
  105. return options.compareTestSnapshot({belongsTo, expected, index, label});
  106. };
  107. this.skipSnapshot = () => {
  108. if (options.updateSnapshots) {
  109. this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
  110. } else {
  111. this.snapshotInvocationCount++;
  112. this.countPassedAssertion();
  113. }
  114. };
  115. this.assertCount = 0;
  116. this.assertError = undefined;
  117. this.calledEnd = false;
  118. this.duration = null;
  119. this.endCallbackFinisher = null;
  120. this.finishDueToAttributedError = null;
  121. this.finishDueToInactivity = null;
  122. this.finishing = false;
  123. this.pendingAssertionCount = 0;
  124. this.pendingThrowsAssertion = null;
  125. this.planCount = null;
  126. this.startedAt = 0;
  127. }
  128. bindEndCallback() {
  129. if (this.metadata.callback) {
  130. return (err, stack) => {
  131. this.endCallback(err, stack);
  132. };
  133. }
  134. throw new Error('`t.end()`` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`');
  135. }
  136. endCallback(err, stack) {
  137. if (this.calledEnd) {
  138. this.saveFirstError(new Error('`t.end()` called more than once'));
  139. return;
  140. }
  141. this.calledEnd = true;
  142. if (err) {
  143. this.saveFirstError(new assert.AssertionError({
  144. actual: err,
  145. message: 'Callback called with an error',
  146. stack,
  147. values: [formatErrorValue('Callback called with an error:', err)]
  148. }));
  149. }
  150. if (this.endCallbackFinisher) {
  151. this.endCallbackFinisher();
  152. }
  153. }
  154. createExecutionContext() {
  155. return new ExecutionContext(this);
  156. }
  157. countPassedAssertion() {
  158. if (this.finishing) {
  159. this.saveFirstError(new Error('Assertion passed, but test has already finished'));
  160. }
  161. this.assertCount++;
  162. }
  163. addLog(text) {
  164. this.logs.push(text);
  165. }
  166. addPendingAssertion(promise) {
  167. if (this.finishing) {
  168. this.saveFirstError(new Error('Assertion passed, but test has already finished'));
  169. }
  170. this.assertCount++;
  171. this.pendingAssertionCount++;
  172. promise
  173. .catch(err => this.saveFirstError(err))
  174. .then(() => this.pendingAssertionCount--);
  175. }
  176. addFailedAssertion(error) {
  177. if (this.finishing) {
  178. this.saveFirstError(new Error('Assertion failed, but test has already finished'));
  179. }
  180. this.assertCount++;
  181. this.saveFirstError(error);
  182. }
  183. saveFirstError(err) {
  184. if (!this.assertError) {
  185. this.assertError = err;
  186. }
  187. }
  188. plan(count, planStack) {
  189. if (typeof count !== 'number') {
  190. throw new TypeError('Expected a number');
  191. }
  192. this.planCount = count;
  193. // In case the `planCount` doesn't match `assertCount, we need the stack of
  194. // this function to throw with a useful stack.
  195. this.planStack = planStack;
  196. }
  197. verifyPlan() {
  198. if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
  199. this.saveFirstError(new assert.AssertionError({
  200. assertion: 'plan',
  201. message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`,
  202. operator: '===',
  203. stack: this.planStack
  204. }));
  205. }
  206. }
  207. verifyAssertions() {
  208. if (!this.assertError) {
  209. if (this.failWithoutAssertions && !this.calledEnd && this.planCount === null && this.assertCount === 0) {
  210. this.saveFirstError(new Error('Test finished without running any assertions'));
  211. } else if (this.pendingAssertionCount > 0) {
  212. this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
  213. }
  214. }
  215. }
  216. trackThrows(pending) {
  217. this.pendingThrowsAssertion = pending;
  218. }
  219. detectImproperThrows(err) {
  220. if (!this.pendingThrowsAssertion) {
  221. return false;
  222. }
  223. const pending = this.pendingThrowsAssertion;
  224. this.pendingThrowsAssertion = null;
  225. const values = [];
  226. if (err) {
  227. values.push(formatErrorValue(`The following error was thrown, possibly before \`t.${pending.assertion}()\` could be called:`, err));
  228. }
  229. this.saveFirstError(new assert.AssertionError({
  230. assertion: pending.assertion,
  231. fixedSource: {file: pending.file, line: pending.line},
  232. improperUsage: true,
  233. message: `Improper usage of \`t.${pending.assertion}()\` detected`,
  234. stack: err instanceof Error && err.stack,
  235. values
  236. }));
  237. return true;
  238. }
  239. waitForPendingThrowsAssertion() {
  240. return new Promise(resolve => {
  241. this.finishDueToAttributedError = () => {
  242. resolve(this.finishPromised());
  243. };
  244. this.finishDueToInactivity = () => {
  245. this.detectImproperThrows();
  246. resolve(this.finishPromised());
  247. };
  248. // Wait up to a second to see if an error can be attributed to the
  249. // pending assertion.
  250. nowAndTimers.setTimeout(() => this.finishDueToInactivity(), 1000).unref();
  251. });
  252. }
  253. attributeLeakedError(err) {
  254. if (!this.detectImproperThrows(err)) {
  255. return false;
  256. }
  257. this.finishDueToAttributedError();
  258. return true;
  259. }
  260. callFn() {
  261. try {
  262. return {
  263. ok: true,
  264. retval: this.fn.call(null, this.createExecutionContext())
  265. };
  266. } catch (err) {
  267. return {
  268. ok: false,
  269. error: err
  270. };
  271. }
  272. }
  273. run() {
  274. this.startedAt = nowAndTimers.now();
  275. const result = this.callFn();
  276. if (!result.ok) {
  277. if (!this.detectImproperThrows(result.error)) {
  278. this.saveFirstError(new assert.AssertionError({
  279. message: 'Error thrown in test',
  280. stack: result.error instanceof Error && result.error.stack,
  281. values: [formatErrorValue('Error thrown in test:', result.error)]
  282. }));
  283. }
  284. return this.finishPromised();
  285. }
  286. const returnedObservable = isObservable(result.retval);
  287. const returnedPromise = isPromise(result.retval);
  288. let promise;
  289. if (returnedObservable) {
  290. promise = observableToPromise(result.retval);
  291. } else if (returnedPromise) {
  292. // `retval` can be any thenable, so convert to a proper promise.
  293. promise = Promise.resolve(result.retval);
  294. }
  295. if (this.metadata.callback) {
  296. if (returnedObservable || returnedPromise) {
  297. const asyncType = returnedObservable ? 'observables' : 'promises';
  298. this.saveFirstError(new Error(`Do not return ${asyncType} from tests declared via \`test.cb(...)\`, if you want to return a promise simply declare the test via \`test(...)\``));
  299. return this.finishPromised();
  300. }
  301. if (this.calledEnd) {
  302. return this.finishPromised();
  303. }
  304. return new Promise(resolve => {
  305. this.endCallbackFinisher = () => {
  306. resolve(this.finishPromised());
  307. };
  308. this.finishDueToAttributedError = () => {
  309. resolve(this.finishPromised());
  310. };
  311. this.finishDueToInactivity = () => {
  312. this.saveFirstError(new Error('`t.end()` was never called'));
  313. resolve(this.finishPromised());
  314. };
  315. });
  316. }
  317. if (promise) {
  318. return new Promise(resolve => {
  319. this.finishDueToAttributedError = () => {
  320. resolve(this.finishPromised());
  321. };
  322. this.finishDueToInactivity = () => {
  323. const err = returnedObservable ?
  324. new Error('Observable returned by test never completed') :
  325. new Error('Promise returned by test never resolved');
  326. this.saveFirstError(err);
  327. resolve(this.finishPromised());
  328. };
  329. promise
  330. .catch(err => {
  331. if (!this.detectImproperThrows(err)) {
  332. this.saveFirstError(new assert.AssertionError({
  333. message: 'Rejected promise returned by test',
  334. stack: err instanceof Error && err.stack,
  335. values: [formatErrorValue('Rejected promise returned by test. Reason:', err)]
  336. }));
  337. }
  338. })
  339. .then(() => resolve(this.finishPromised()));
  340. });
  341. }
  342. return this.finishPromised();
  343. }
  344. finish() {
  345. this.finishing = true;
  346. if (!this.assertError && this.pendingThrowsAssertion) {
  347. return this.waitForPendingThrowsAssertion();
  348. }
  349. this.verifyPlan();
  350. this.verifyAssertions();
  351. this.duration = nowAndTimers.now() - this.startedAt;
  352. let error = this.assertError;
  353. let passed = !error;
  354. if (this.metadata.failing) {
  355. passed = !passed;
  356. if (passed) {
  357. error = null;
  358. } else {
  359. error = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
  360. }
  361. }
  362. return {
  363. duration: this.duration,
  364. error,
  365. logs: this.logs,
  366. metadata: this.metadata,
  367. passed,
  368. title: this.title
  369. };
  370. }
  371. finishPromised() {
  372. return new Promise(resolve => {
  373. resolve(this.finish());
  374. });
  375. }
  376. }
  377. module.exports = Test;