assert.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. 'use strict';
  2. const concordance = require('concordance');
  3. const observableToPromise = require('observable-to-promise');
  4. const isError = require('is-error');
  5. const isObservable = require('is-observable');
  6. const isPromise = require('is-promise');
  7. const concordanceOptions = require('./concordance-options').default;
  8. const concordanceDiffOptions = require('./concordance-options').diff;
  9. const enhanceAssert = require('./enhance-assert');
  10. const snapshotManager = require('./snapshot-manager');
  11. function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
  12. options = Object.assign({}, options, concordanceDiffOptions);
  13. return {
  14. label: 'Difference:',
  15. formatted: concordance.diffDescriptors(actualDescriptor, expectedDescriptor, options)
  16. };
  17. }
  18. function formatDescriptorWithLabel(label, descriptor) {
  19. return {
  20. label,
  21. formatted: concordance.formatDescriptor(descriptor, concordanceOptions)
  22. };
  23. }
  24. function formatWithLabel(label, value) {
  25. return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
  26. }
  27. const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
  28. class AssertionError extends Error {
  29. constructor(opts) {
  30. super(opts.message || '');
  31. this.name = 'AssertionError';
  32. this.assertion = opts.assertion;
  33. this.fixedSource = opts.fixedSource;
  34. this.improperUsage = opts.improperUsage || false;
  35. this.operator = opts.operator;
  36. this.values = opts.values || [];
  37. // Raw expected and actual objects are stored for custom reporters
  38. // (such as wallaby.js), that manage worker processes directly and
  39. // use the values for custom diff views
  40. this.raw = opts.raw;
  41. // Reserved for power-assert statements
  42. this.statements = [];
  43. if (opts.stack) {
  44. this.stack = opts.stack;
  45. } else {
  46. const limitBefore = Error.stackTraceLimit;
  47. Error.stackTraceLimit = Infinity;
  48. Error.captureStackTrace(this);
  49. Error.stackTraceLimit = limitBefore;
  50. }
  51. }
  52. }
  53. exports.AssertionError = AssertionError;
  54. function getStack() {
  55. const limitBefore = Error.stackTraceLimit;
  56. Error.stackTraceLimit = Infinity;
  57. const obj = {};
  58. Error.captureStackTrace(obj, getStack);
  59. Error.stackTraceLimit = limitBefore;
  60. return obj.stack;
  61. }
  62. function wrapAssertions(callbacks) {
  63. const pass = callbacks.pass;
  64. const pending = callbacks.pending;
  65. const fail = callbacks.fail;
  66. const noop = () => {};
  67. const assertions = {
  68. pass() {
  69. pass(this);
  70. },
  71. fail(message) {
  72. fail(this, new AssertionError({
  73. assertion: 'fail',
  74. message: message || 'Test failed via `t.fail()`'
  75. }));
  76. },
  77. is(actual, expected, message) {
  78. if (Object.is(actual, expected)) {
  79. pass(this);
  80. } else {
  81. const result = concordance.compare(actual, expected, concordanceOptions);
  82. const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
  83. const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
  84. if (result.pass) {
  85. fail(this, new AssertionError({
  86. assertion: 'is',
  87. message,
  88. raw: {actual, expected},
  89. values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)]
  90. }));
  91. } else {
  92. fail(this, new AssertionError({
  93. assertion: 'is',
  94. message,
  95. raw: {actual, expected},
  96. values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
  97. }));
  98. }
  99. }
  100. },
  101. not(actual, expected, message) {
  102. if (Object.is(actual, expected)) {
  103. fail(this, new AssertionError({
  104. assertion: 'not',
  105. message,
  106. raw: {actual, expected},
  107. values: [formatWithLabel('Value is the same as:', actual)]
  108. }));
  109. } else {
  110. pass(this);
  111. }
  112. },
  113. deepEqual(actual, expected, message) {
  114. const result = concordance.compare(actual, expected, concordanceOptions);
  115. if (result.pass) {
  116. pass(this);
  117. } else {
  118. const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
  119. const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
  120. fail(this, new AssertionError({
  121. assertion: 'deepEqual',
  122. message,
  123. raw: {actual, expected},
  124. values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
  125. }));
  126. }
  127. },
  128. notDeepEqual(actual, expected, message) {
  129. const result = concordance.compare(actual, expected, concordanceOptions);
  130. if (result.pass) {
  131. const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
  132. fail(this, new AssertionError({
  133. assertion: 'notDeepEqual',
  134. message,
  135. raw: {actual, expected},
  136. values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)]
  137. }));
  138. } else {
  139. pass(this);
  140. }
  141. },
  142. throws(thrower, expected, message) { // eslint-disable-line complexity
  143. if (typeof thrower !== 'function' && !isPromise(thrower) && !isObservable(thrower)) {
  144. fail(this, new AssertionError({
  145. assertion: 'throws',
  146. improperUsage: true,
  147. message: '`t.throws()` must be called with a function, observable or promise',
  148. values: [formatWithLabel('Called with:', thrower)]
  149. }));
  150. return;
  151. }
  152. if (typeof expected === 'function') {
  153. expected = {instanceOf: expected};
  154. } else if (typeof expected === 'string' || expected instanceof RegExp) {
  155. expected = {message: expected};
  156. } else if (arguments.length === 1 || expected === null) {
  157. expected = {};
  158. } else if (typeof expected !== 'object' || Array.isArray(expected) || Object.keys(expected).length === 0) {
  159. fail(this, new AssertionError({
  160. assertion: 'throws',
  161. message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`',
  162. values: [formatWithLabel('Called with:', expected)]
  163. }));
  164. return;
  165. } else {
  166. if (hasOwnProperty(expected, 'instanceOf') && typeof expected.instanceOf !== 'function') {
  167. fail(this, new AssertionError({
  168. assertion: 'throws',
  169. message: 'The `instanceOf` property of the second argument to `t.throws()` must be a function',
  170. values: [formatWithLabel('Called with:', expected)]
  171. }));
  172. return;
  173. }
  174. if (hasOwnProperty(expected, 'message') && typeof expected.message !== 'string' && !(expected.message instanceof RegExp)) {
  175. fail(this, new AssertionError({
  176. assertion: 'throws',
  177. message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression',
  178. values: [formatWithLabel('Called with:', expected)]
  179. }));
  180. return;
  181. }
  182. if (hasOwnProperty(expected, 'name') && typeof expected.name !== 'string') {
  183. fail(this, new AssertionError({
  184. assertion: 'throws',
  185. message: 'The `name` property of the second argument to `t.throws()` must be a string',
  186. values: [formatWithLabel('Called with:', expected)]
  187. }));
  188. return;
  189. }
  190. if (hasOwnProperty(expected, 'code') && typeof expected.code !== 'string') {
  191. fail(this, new AssertionError({
  192. assertion: 'throws',
  193. message: 'The `code` property of the second argument to `t.throws()` must be a string',
  194. values: [formatWithLabel('Called with:', expected)]
  195. }));
  196. return;
  197. }
  198. for (const key of Object.keys(expected)) {
  199. switch (key) {
  200. case 'instanceOf':
  201. case 'is':
  202. case 'message':
  203. case 'name':
  204. case 'code':
  205. continue;
  206. default:
  207. fail(this, new AssertionError({
  208. assertion: 'throws',
  209. message: 'The second argument to `t.throws()` contains unexpected properties',
  210. values: [formatWithLabel('Called with:', expected)]
  211. }));
  212. return;
  213. }
  214. }
  215. }
  216. // Note: this function *must* throw exceptions, since it can be used
  217. // as part of a pending assertion for observables and promises.
  218. const assertExpected = (actual, prefix, stack) => {
  219. if (!isError(actual)) {
  220. throw new AssertionError({
  221. assertion: 'throws',
  222. message,
  223. stack,
  224. values: [formatWithLabel(`${prefix} exception that is not an error:`, actual)]
  225. });
  226. }
  227. if (hasOwnProperty(expected, 'is') && actual !== expected.is) {
  228. throw new AssertionError({
  229. assertion: 'throws',
  230. message,
  231. stack,
  232. values: [
  233. formatWithLabel(`${prefix} unexpected exception:`, actual),
  234. formatWithLabel('Expected to be strictly equal to:', expected.is)
  235. ]
  236. });
  237. }
  238. if (expected.instanceOf && !(actual instanceof expected.instanceOf)) {
  239. throw new AssertionError({
  240. assertion: 'throws',
  241. message,
  242. stack,
  243. values: [
  244. formatWithLabel(`${prefix} unexpected exception:`, actual),
  245. formatWithLabel('Expected instance of:', expected.instanceOf)
  246. ]
  247. });
  248. }
  249. if (typeof expected.name === 'string' && actual.name !== expected.name) {
  250. throw new AssertionError({
  251. assertion: 'throws',
  252. message,
  253. stack,
  254. values: [
  255. formatWithLabel(`${prefix} unexpected exception:`, actual),
  256. formatWithLabel('Expected name to equal:', expected.name)
  257. ]
  258. });
  259. }
  260. if (typeof expected.message === 'string' && actual.message !== expected.message) {
  261. throw new AssertionError({
  262. assertion: 'throws',
  263. message,
  264. stack,
  265. values: [
  266. formatWithLabel(`${prefix} unexpected exception:`, actual),
  267. formatWithLabel('Expected message to equal:', expected.message)
  268. ]
  269. });
  270. }
  271. if (expected.message instanceof RegExp && !expected.message.test(actual.message)) {
  272. throw new AssertionError({
  273. assertion: 'throws',
  274. message,
  275. stack,
  276. values: [
  277. formatWithLabel(`${prefix} unexpected exception:`, actual),
  278. formatWithLabel('Expected message to match:', expected.message)
  279. ]
  280. });
  281. }
  282. if (typeof expected.code === 'string' && actual.code !== expected.code) {
  283. throw new AssertionError({
  284. assertion: 'throws',
  285. message,
  286. stack,
  287. values: [
  288. formatWithLabel(`${prefix} unexpected exception:`, actual),
  289. formatWithLabel('Expected code to equal:', expected.code)
  290. ]
  291. });
  292. }
  293. };
  294. const handleObservable = (observable, wasReturned) => {
  295. // Record stack before it gets lost in the promise chain.
  296. const stack = getStack();
  297. const intermediate = observableToPromise(observable).then(value => {
  298. throw new AssertionError({
  299. assertion: 'throws',
  300. message,
  301. stack,
  302. values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} completed with:`, value)]
  303. });
  304. }, reason => {
  305. assertExpected(reason, `${wasReturned ? 'Returned observable' : 'Observable'} errored with`, stack);
  306. return reason;
  307. });
  308. pending(this, intermediate);
  309. // Don't reject the returned promise, even if the assertion fails.
  310. return intermediate.catch(noop);
  311. };
  312. const handlePromise = (promise, wasReturned) => {
  313. // Record stack before it gets lost in the promise chain.
  314. const stack = getStack();
  315. const intermediate = promise.then(value => {
  316. throw new AssertionError({
  317. assertion: 'throws',
  318. message,
  319. stack,
  320. values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} resolved with:`, value)]
  321. });
  322. }, reason => {
  323. assertExpected(reason, `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, stack);
  324. return reason;
  325. });
  326. pending(this, intermediate);
  327. // Don't reject the returned promise, even if the assertion fails.
  328. return intermediate.catch(noop);
  329. };
  330. if (isPromise(thrower)) {
  331. return handlePromise(thrower, false);
  332. }
  333. if (isObservable(thrower)) {
  334. return handleObservable(thrower, false);
  335. }
  336. let retval;
  337. let actual;
  338. let threw = false;
  339. try {
  340. retval = thrower();
  341. } catch (err) {
  342. actual = err;
  343. threw = true;
  344. }
  345. if (!threw) {
  346. if (isPromise(retval)) {
  347. return handlePromise(retval, true);
  348. }
  349. if (isObservable(retval)) {
  350. return handleObservable(retval, true);
  351. }
  352. fail(this, new AssertionError({
  353. assertion: 'throws',
  354. message,
  355. values: [formatWithLabel('Function returned:', retval)]
  356. }));
  357. return;
  358. }
  359. try {
  360. assertExpected(actual, 'Function threw');
  361. pass(this);
  362. return actual;
  363. } catch (err) {
  364. fail(this, err);
  365. }
  366. },
  367. notThrows(nonThrower, message) {
  368. if (typeof nonThrower !== 'function' && !isPromise(nonThrower) && !isObservable(nonThrower)) {
  369. fail(this, new AssertionError({
  370. assertion: 'notThrows',
  371. improperUsage: true,
  372. message: '`t.notThrows()` must be called with a function, observable or promise',
  373. values: [formatWithLabel('Called with:', nonThrower)]
  374. }));
  375. return;
  376. }
  377. const handleObservable = (observable, wasReturned) => {
  378. // Record stack before it gets lost in the promise chain.
  379. const stack = getStack();
  380. const intermediate = observableToPromise(observable).then(noop, reason => {
  381. throw new AssertionError({
  382. assertion: 'notThrows',
  383. message,
  384. stack,
  385. values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} errored with:`, reason)]
  386. });
  387. });
  388. pending(this, intermediate);
  389. // Don't reject the returned promise, even if the assertion fails.
  390. return intermediate.catch(noop);
  391. };
  392. const handlePromise = (promise, wasReturned) => {
  393. // Record stack before it gets lost in the promise chain.
  394. const stack = getStack();
  395. const intermediate = promise.then(noop, reason => {
  396. throw new AssertionError({
  397. assertion: 'notThrows',
  398. message,
  399. stack,
  400. values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, reason)]
  401. });
  402. });
  403. pending(this, intermediate);
  404. // Don't reject the returned promise, even if the assertion fails.
  405. return intermediate.catch(noop);
  406. };
  407. if (isPromise(nonThrower)) {
  408. return handlePromise(nonThrower, false);
  409. }
  410. if (isObservable(nonThrower)) {
  411. return handleObservable(nonThrower, false);
  412. }
  413. let retval;
  414. try {
  415. retval = nonThrower();
  416. } catch (err) {
  417. fail(this, new AssertionError({
  418. assertion: 'notThrows',
  419. message,
  420. values: [formatWithLabel(`Function threw:`, err)]
  421. }));
  422. return;
  423. }
  424. if (isPromise(retval)) {
  425. return handlePromise(retval, true);
  426. }
  427. if (isObservable(retval)) {
  428. return handleObservable(retval, true);
  429. }
  430. pass(this);
  431. },
  432. snapshot(expected, optionsOrMessage, message) {
  433. const options = {};
  434. if (typeof optionsOrMessage === 'string') {
  435. message = optionsOrMessage;
  436. } else if (optionsOrMessage) {
  437. options.id = optionsOrMessage.id;
  438. }
  439. options.expected = expected;
  440. options.message = message;
  441. let result;
  442. try {
  443. result = this.compareWithSnapshot(options);
  444. } catch (err) {
  445. if (!(err instanceof snapshotManager.SnapshotError)) {
  446. throw err;
  447. }
  448. const improperUsage = {name: err.name, snapPath: err.snapPath};
  449. if (err instanceof snapshotManager.VersionMismatchError) {
  450. improperUsage.snapVersion = err.snapVersion;
  451. improperUsage.expectedVersion = err.expectedVersion;
  452. }
  453. fail(this, new AssertionError({
  454. assertion: 'snapshot',
  455. message: message || 'Could not compare snapshot',
  456. improperUsage
  457. }));
  458. return;
  459. }
  460. if (result.pass) {
  461. pass(this);
  462. } else if (result.actual) {
  463. fail(this, new AssertionError({
  464. assertion: 'snapshot',
  465. message: message || 'Did not match snapshot',
  466. values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})]
  467. }));
  468. } else {
  469. fail(this, new AssertionError({
  470. assertion: 'snapshot',
  471. message: message || 'No snapshot available, run with --update-snapshots'
  472. }));
  473. }
  474. }
  475. };
  476. const enhancedAssertions = enhanceAssert(pass, fail, {
  477. truthy(actual, message) {
  478. if (!actual) {
  479. throw new AssertionError({
  480. assertion: 'truthy',
  481. message,
  482. operator: '!!',
  483. values: [formatWithLabel('Value is not truthy:', actual)]
  484. });
  485. }
  486. },
  487. falsy(actual, message) {
  488. if (actual) {
  489. throw new AssertionError({
  490. assertion: 'falsy',
  491. message,
  492. operator: '!',
  493. values: [formatWithLabel('Value is not falsy:', actual)]
  494. });
  495. }
  496. },
  497. true(actual, message) {
  498. if (actual !== true) {
  499. throw new AssertionError({
  500. assertion: 'true',
  501. message,
  502. values: [formatWithLabel('Value is not `true`:', actual)]
  503. });
  504. }
  505. },
  506. false(actual, message) {
  507. if (actual !== false) {
  508. throw new AssertionError({
  509. assertion: 'false',
  510. message,
  511. values: [formatWithLabel('Value is not `false`:', actual)]
  512. });
  513. }
  514. },
  515. regex(string, regex, message) {
  516. if (typeof string !== 'string') {
  517. throw new AssertionError({
  518. assertion: 'regex',
  519. improperUsage: true,
  520. message: '`t.regex()` must be called with a string',
  521. values: [formatWithLabel('Called with:', string)]
  522. });
  523. }
  524. if (!(regex instanceof RegExp)) {
  525. throw new AssertionError({
  526. assertion: 'regex',
  527. improperUsage: true,
  528. message: '`t.regex()` must be called with a regular expression',
  529. values: [formatWithLabel('Called with:', regex)]
  530. });
  531. }
  532. if (!regex.test(string)) {
  533. throw new AssertionError({
  534. assertion: 'regex',
  535. message,
  536. values: [
  537. formatWithLabel('Value must match expression:', string),
  538. formatWithLabel('Regular expression:', regex)
  539. ]
  540. });
  541. }
  542. },
  543. notRegex(string, regex, message) {
  544. if (typeof string !== 'string') {
  545. throw new AssertionError({
  546. assertion: 'notRegex',
  547. improperUsage: true,
  548. message: '`t.notRegex()` must be called with a string',
  549. values: [formatWithLabel('Called with:', string)]
  550. });
  551. }
  552. if (!(regex instanceof RegExp)) {
  553. throw new AssertionError({
  554. assertion: 'notRegex',
  555. improperUsage: true,
  556. message: '`t.notRegex()` must be called with a regular expression',
  557. values: [formatWithLabel('Called with:', regex)]
  558. });
  559. }
  560. if (regex.test(string)) {
  561. throw new AssertionError({
  562. assertion: 'notRegex',
  563. message,
  564. values: [
  565. formatWithLabel('Value must not match expression:', string),
  566. formatWithLabel('Regular expression:', regex)
  567. ]
  568. });
  569. }
  570. }
  571. });
  572. return Object.assign(assertions, enhancedAssertions);
  573. }
  574. exports.wrapAssertions = wrapAssertions;