UNPKG

23.2 kBJavaScriptView Raw
1'use strict';
2const concordance = require('concordance');
3const isError = require('is-error');
4const isPromise = require('is-promise');
5const concordanceOptions = require('./concordance-options').default;
6const concordanceDiffOptions = require('./concordance-options').diff;
7const snapshotManager = require('./snapshot-manager');
8
9function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
10 options = {...options, ...concordanceDiffOptions};
11 return {
12 label: 'Difference:',
13 formatted: concordance.diffDescriptors(actualDescriptor, expectedDescriptor, options)
14 };
15}
16
17function formatDescriptorWithLabel(label, descriptor) {
18 return {
19 label,
20 formatted: concordance.formatDescriptor(descriptor, concordanceOptions)
21 };
22}
23
24function formatWithLabel(label, value) {
25 return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
26}
27
28function formatPowerAssertValue(value) {
29 return concordance.format(value, concordanceOptions);
30}
31
32const hasOwnProperty = (object, prop) => Object.prototype.hasOwnProperty.call(object, prop);
33const noop = () => {};
34const notImplemented = () => {
35 throw new Error('not implemented');
36};
37
38class AssertionError extends Error {
39 constructor(options) {
40 super(options.message || '');
41 this.name = 'AssertionError';
42
43 this.assertion = options.assertion;
44 this.fixedSource = options.fixedSource;
45 this.improperUsage = options.improperUsage || false;
46 this.actualStack = options.actualStack;
47 this.operator = options.operator;
48 this.values = options.values || [];
49
50 // Raw expected and actual objects are stored for custom reporters
51 // (such as wallaby.js), that manage worker processes directly and
52 // use the values for custom diff views
53 this.raw = options.raw;
54
55 // Reserved for power-assert statements
56 this.statements = [];
57
58 if (options.savedError) {
59 this.savedError = options.savedError;
60 } else {
61 this.savedError = getErrorWithLongStackTrace();
62 }
63 }
64}
65exports.AssertionError = AssertionError;
66
67function getErrorWithLongStackTrace() {
68 const limitBefore = Error.stackTraceLimit;
69 Error.stackTraceLimit = Infinity;
70 const err = new Error();
71 Error.stackTraceLimit = limitBefore;
72 return err;
73}
74
75function validateExpectations(assertion, expectations, numberArgs) { // eslint-disable-line complexity
76 if (numberArgs === 1 || expectations === null || expectations === undefined) {
77 expectations = {};
78 } else if (
79 typeof expectations === 'function' ||
80 typeof expectations === 'string' ||
81 expectations instanceof RegExp ||
82 typeof expectations !== 'object' ||
83 Array.isArray(expectations) ||
84 Object.keys(expectations).length === 0
85 ) {
86 throw new AssertionError({
87 assertion,
88 message: `The second argument to \`t.${assertion}()\` must be an expectation object, \`null\` or \`undefined\``,
89 values: [formatWithLabel('Called with:', expectations)]
90 });
91 } else {
92 if (hasOwnProperty(expectations, 'instanceOf') && typeof expectations.instanceOf !== 'function') {
93 throw new AssertionError({
94 assertion,
95 message: `The \`instanceOf\` property of the second argument to \`t.${assertion}()\` must be a function`,
96 values: [formatWithLabel('Called with:', expectations)]
97 });
98 }
99
100 if (hasOwnProperty(expectations, 'message') && typeof expectations.message !== 'string' && !(expectations.message instanceof RegExp)) {
101 throw new AssertionError({
102 assertion,
103 message: `The \`message\` property of the second argument to \`t.${assertion}()\` must be a string or regular expression`,
104 values: [formatWithLabel('Called with:', expectations)]
105 });
106 }
107
108 if (hasOwnProperty(expectations, 'name') && typeof expectations.name !== 'string') {
109 throw new AssertionError({
110 assertion,
111 message: `The \`name\` property of the second argument to \`t.${assertion}()\` must be a string`,
112 values: [formatWithLabel('Called with:', expectations)]
113 });
114 }
115
116 if (hasOwnProperty(expectations, 'code') && typeof expectations.code !== 'string' && typeof expectations.code !== 'number') {
117 throw new AssertionError({
118 assertion,
119 message: `The \`code\` property of the second argument to \`t.${assertion}()\` must be a string or number`,
120 values: [formatWithLabel('Called with:', expectations)]
121 });
122 }
123
124 for (const key of Object.keys(expectations)) {
125 switch (key) {
126 case 'instanceOf':
127 case 'is':
128 case 'message':
129 case 'name':
130 case 'code':
131 continue;
132 default:
133 throw new AssertionError({
134 assertion,
135 message: `The second argument to \`t.${assertion}()\` contains unexpected properties`,
136 values: [formatWithLabel('Called with:', expectations)]
137 });
138 }
139 }
140 }
141
142 return expectations;
143}
144
145// Note: this function *must* throw exceptions, since it can be used
146// as part of a pending assertion for promises.
147function assertExpectations({assertion, actual, expectations, message, prefix, savedError}) {
148 if (!isError(actual)) {
149 throw new AssertionError({
150 assertion,
151 message,
152 savedError,
153 values: [formatWithLabel(`${prefix} exception that is not an error:`, actual)]
154 });
155 }
156
157 const actualStack = actual.stack;
158
159 if (hasOwnProperty(expectations, 'is') && actual !== expectations.is) {
160 throw new AssertionError({
161 assertion,
162 message,
163 savedError,
164 actualStack,
165 values: [
166 formatWithLabel(`${prefix} unexpected exception:`, actual),
167 formatWithLabel('Expected to be strictly equal to:', expectations.is)
168 ]
169 });
170 }
171
172 if (expectations.instanceOf && !(actual instanceof expectations.instanceOf)) {
173 throw new AssertionError({
174 assertion,
175 message,
176 savedError,
177 actualStack,
178 values: [
179 formatWithLabel(`${prefix} unexpected exception:`, actual),
180 formatWithLabel('Expected instance of:', expectations.instanceOf)
181 ]
182 });
183 }
184
185 if (typeof expectations.name === 'string' && actual.name !== expectations.name) {
186 throw new AssertionError({
187 assertion,
188 message,
189 savedError,
190 actualStack,
191 values: [
192 formatWithLabel(`${prefix} unexpected exception:`, actual),
193 formatWithLabel('Expected name to equal:', expectations.name)
194 ]
195 });
196 }
197
198 if (typeof expectations.message === 'string' && actual.message !== expectations.message) {
199 throw new AssertionError({
200 assertion,
201 message,
202 savedError,
203 actualStack,
204 values: [
205 formatWithLabel(`${prefix} unexpected exception:`, actual),
206 formatWithLabel('Expected message to equal:', expectations.message)
207 ]
208 });
209 }
210
211 if (expectations.message instanceof RegExp && !expectations.message.test(actual.message)) {
212 throw new AssertionError({
213 assertion,
214 message,
215 savedError,
216 actualStack,
217 values: [
218 formatWithLabel(`${prefix} unexpected exception:`, actual),
219 formatWithLabel('Expected message to match:', expectations.message)
220 ]
221 });
222 }
223
224 if (typeof expectations.code !== 'undefined' && actual.code !== expectations.code) {
225 throw new AssertionError({
226 assertion,
227 message,
228 savedError,
229 actualStack,
230 values: [
231 formatWithLabel(`${prefix} unexpected exception:`, actual),
232 formatWithLabel('Expected code to equal:', expectations.code)
233 ]
234 });
235 }
236}
237
238class Assertions {
239 constructor({
240 pass = notImplemented,
241 pending = notImplemented,
242 fail = notImplemented,
243 skip = notImplemented,
244 compareWithSnapshot = notImplemented,
245 powerAssert
246 } = {}) {
247 const withSkip = assertionFn => {
248 assertionFn.skip = skip;
249 return assertionFn;
250 };
251
252 // When adding new enhanced functions with new patterns, don't forget to
253 // enable the pattern in the power-assert compilation step in @ava/babel.
254 const withPowerAssert = (pattern, assertionFn) => powerAssert.empower(assertionFn, {
255 onError: event => {
256 if (event.powerAssertContext) {
257 event.error.statements = powerAssert.format(event.powerAssertContext, formatPowerAssertValue);
258 }
259
260 fail(event.error);
261 },
262 onSuccess: () => {
263 pass();
264 },
265 bindReceiver: false,
266 patterns: [pattern]
267 });
268
269 const checkMessage = (assertion, message, powerAssert = false) => {
270 if (typeof message === 'undefined' || typeof message === 'string') {
271 return true;
272 }
273
274 const error = new AssertionError({
275 assertion,
276 improperUsage: true,
277 message: 'The assertion message must be a string',
278 values: [formatWithLabel('Called with:', message)]
279 });
280
281 if (powerAssert) {
282 throw error;
283 }
284
285 fail(error);
286 return false;
287 };
288
289 this.pass = withSkip(() => {
290 pass();
291 });
292
293 this.fail = withSkip(message => {
294 if (!checkMessage('fail', message)) {
295 return;
296 }
297
298 fail(new AssertionError({
299 assertion: 'fail',
300 message: message || 'Test failed via `t.fail()`'
301 }));
302 });
303
304 this.is = withSkip((actual, expected, message) => {
305 if (!checkMessage('is', message)) {
306 return;
307 }
308
309 if (Object.is(actual, expected)) {
310 pass();
311 } else {
312 const result = concordance.compare(actual, expected, concordanceOptions);
313 const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
314 const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
315
316 if (result.pass) {
317 fail(new AssertionError({
318 assertion: 'is',
319 message,
320 raw: {actual, expected},
321 values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)]
322 }));
323 } else {
324 fail(new AssertionError({
325 assertion: 'is',
326 message,
327 raw: {actual, expected},
328 values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
329 }));
330 }
331 }
332 });
333
334 this.not = withSkip((actual, expected, message) => {
335 if (!checkMessage('not', message)) {
336 return;
337 }
338
339 if (Object.is(actual, expected)) {
340 fail(new AssertionError({
341 assertion: 'not',
342 message,
343 raw: {actual, expected},
344 values: [formatWithLabel('Value is the same as:', actual)]
345 }));
346 } else {
347 pass();
348 }
349 });
350
351 this.deepEqual = withSkip((actual, expected, message) => {
352 if (!checkMessage('deepEqual', message)) {
353 return;
354 }
355
356 const result = concordance.compare(actual, expected, concordanceOptions);
357 if (result.pass) {
358 pass();
359 } else {
360 const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
361 const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
362 fail(new AssertionError({
363 assertion: 'deepEqual',
364 message,
365 raw: {actual, expected},
366 values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
367 }));
368 }
369 });
370
371 this.notDeepEqual = withSkip((actual, expected, message) => {
372 if (!checkMessage('notDeepEqual', message)) {
373 return;
374 }
375
376 const result = concordance.compare(actual, expected, concordanceOptions);
377 if (result.pass) {
378 const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
379 fail(new AssertionError({
380 assertion: 'notDeepEqual',
381 message,
382 raw: {actual, expected},
383 values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)]
384 }));
385 } else {
386 pass();
387 }
388 });
389
390 this.throws = withSkip((...args) => {
391 // Since arrow functions do not support 'arguments', we are using rest
392 // operator, so we can determine the total number of arguments passed
393 // to the function.
394 let [fn, expectations, message] = args;
395
396 if (!checkMessage('throws', message)) {
397 return;
398 }
399
400 if (typeof fn !== 'function') {
401 fail(new AssertionError({
402 assertion: 'throws',
403 improperUsage: true,
404 message: '`t.throws()` must be called with a function',
405 values: [formatWithLabel('Called with:', fn)]
406 }));
407 return;
408 }
409
410 try {
411 expectations = validateExpectations('throws', expectations, args.length);
412 } catch (error) {
413 fail(error);
414 return;
415 }
416
417 let retval;
418 let actual = null;
419 try {
420 retval = fn();
421 if (isPromise(retval)) {
422 // Here isPromise() checks if something is "promise like". Cast to an actual promise.
423 Promise.resolve(retval).catch(noop);
424 fail(new AssertionError({
425 assertion: 'throws',
426 message,
427 values: [formatWithLabel('Function returned a promise. Use `t.throwsAsync()` instead:', retval)]
428 }));
429 return;
430 }
431 } catch (error) {
432 actual = error;
433 }
434
435 if (!actual) {
436 fail(new AssertionError({
437 assertion: 'throws',
438 message,
439 values: [formatWithLabel('Function returned:', retval)]
440 }));
441 return;
442 }
443
444 try {
445 assertExpectations({
446 assertion: 'throws',
447 actual,
448 expectations,
449 message,
450 prefix: 'Function threw'
451 });
452 pass();
453 return actual;
454 } catch (error) {
455 fail(error);
456 }
457 });
458
459 this.throwsAsync = withSkip((...args) => {
460 let [thrower, expectations, message] = args;
461
462 if (!checkMessage('throwsAsync', message)) {
463 return Promise.resolve();
464 }
465
466 if (typeof thrower !== 'function' && !isPromise(thrower)) {
467 fail(new AssertionError({
468 assertion: 'throwsAsync',
469 improperUsage: true,
470 message: '`t.throwsAsync()` must be called with a function or promise',
471 values: [formatWithLabel('Called with:', thrower)]
472 }));
473 return Promise.resolve();
474 }
475
476 try {
477 expectations = validateExpectations('throwsAsync', expectations, args.length);
478 } catch (error) {
479 fail(error);
480 return Promise.resolve();
481 }
482
483 const handlePromise = (promise, wasReturned) => {
484 // Create an error object to record the stack before it gets lost in the promise chain.
485 const savedError = getErrorWithLongStackTrace();
486 // Handle "promise like" objects by casting to a real Promise.
487 const intermediate = Promise.resolve(promise).then(value => { // eslint-disable-line promise/prefer-await-to-then
488 throw new AssertionError({
489 assertion: 'throwsAsync',
490 message,
491 savedError,
492 values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} resolved with:`, value)]
493 });
494 }, error => {
495 assertExpectations({
496 assertion: 'throwsAsync',
497 actual: error,
498 expectations,
499 message,
500 prefix: `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`,
501 savedError
502 });
503 return error;
504 });
505
506 pending(intermediate);
507 // Don't reject the returned promise, even if the assertion fails.
508 return intermediate.catch(noop);
509 };
510
511 if (isPromise(thrower)) {
512 return handlePromise(thrower, false);
513 }
514
515 let retval;
516 let actual = null;
517 try {
518 retval = thrower();
519 } catch (error) {
520 actual = error;
521 }
522
523 if (actual) {
524 fail(new AssertionError({
525 assertion: 'throwsAsync',
526 message,
527 actualStack: actual.stack,
528 values: [formatWithLabel('Function threw synchronously. Use `t.throws()` instead:', actual)]
529 }));
530 return Promise.resolve();
531 }
532
533 if (isPromise(retval)) {
534 return handlePromise(retval, true);
535 }
536
537 fail(new AssertionError({
538 assertion: 'throwsAsync',
539 message,
540 values: [formatWithLabel('Function returned:', retval)]
541 }));
542 return Promise.resolve();
543 });
544
545 this.notThrows = withSkip((fn, message) => {
546 if (!checkMessage('notThrows', message)) {
547 return;
548 }
549
550 if (typeof fn !== 'function') {
551 fail(new AssertionError({
552 assertion: 'notThrows',
553 improperUsage: true,
554 message: '`t.notThrows()` must be called with a function',
555 values: [formatWithLabel('Called with:', fn)]
556 }));
557 return;
558 }
559
560 try {
561 fn();
562 } catch (error) {
563 fail(new AssertionError({
564 assertion: 'notThrows',
565 message,
566 actualStack: error.stack,
567 values: [formatWithLabel('Function threw:', error)]
568 }));
569 return;
570 }
571
572 pass();
573 });
574
575 this.notThrowsAsync = withSkip((nonThrower, message) => {
576 if (!checkMessage('notThrowsAsync', message)) {
577 return Promise.resolve();
578 }
579
580 if (typeof nonThrower !== 'function' && !isPromise(nonThrower)) {
581 fail(new AssertionError({
582 assertion: 'notThrowsAsync',
583 improperUsage: true,
584 message: '`t.notThrowsAsync()` must be called with a function or promise',
585 values: [formatWithLabel('Called with:', nonThrower)]
586 }));
587 return Promise.resolve();
588 }
589
590 const handlePromise = (promise, wasReturned) => {
591 // Create an error object to record the stack before it gets lost in the promise chain.
592 const savedError = getErrorWithLongStackTrace();
593 // Handle "promise like" objects by casting to a real Promise.
594 const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-await-to-then
595 throw new AssertionError({
596 assertion: 'notThrowsAsync',
597 message,
598 savedError,
599 values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, error)]
600 });
601 });
602 pending(intermediate);
603 // Don't reject the returned promise, even if the assertion fails.
604 return intermediate.catch(noop);
605 };
606
607 if (isPromise(nonThrower)) {
608 return handlePromise(nonThrower, false);
609 }
610
611 let retval;
612 try {
613 retval = nonThrower();
614 } catch (error) {
615 fail(new AssertionError({
616 assertion: 'notThrowsAsync',
617 message,
618 actualStack: error.stack,
619 values: [formatWithLabel('Function threw:', error)]
620 }));
621 return Promise.resolve();
622 }
623
624 if (!isPromise(retval)) {
625 fail(new AssertionError({
626 assertion: 'notThrowsAsync',
627 message,
628 values: [formatWithLabel('Function did not return a promise. Use `t.notThrows()` instead:', retval)]
629 }));
630 return Promise.resolve();
631 }
632
633 return handlePromise(retval, true);
634 });
635
636 this.snapshot = withSkip((expected, ...rest) => {
637 let message;
638 let snapshotOptions;
639 if (rest.length > 1) {
640 [snapshotOptions, message] = rest;
641 } else {
642 const [optionsOrMessage] = rest;
643 if (typeof optionsOrMessage === 'object') {
644 snapshotOptions = optionsOrMessage;
645 } else {
646 message = optionsOrMessage;
647 }
648 }
649
650 if (!checkMessage('snapshot', message)) {
651 return;
652 }
653
654 let result;
655 try {
656 result = compareWithSnapshot({
657 expected,
658 id: snapshotOptions ? snapshotOptions.id : undefined,
659 message
660 });
661 } catch (error) {
662 if (!(error instanceof snapshotManager.SnapshotError)) {
663 throw error;
664 }
665
666 const improperUsage = {name: error.name, snapPath: error.snapPath};
667 if (error instanceof snapshotManager.VersionMismatchError) {
668 improperUsage.snapVersion = error.snapVersion;
669 improperUsage.expectedVersion = error.expectedVersion;
670 }
671
672 fail(new AssertionError({
673 assertion: 'snapshot',
674 message: message || 'Could not compare snapshot',
675 improperUsage
676 }));
677 return;
678 }
679
680 if (result.pass) {
681 pass();
682 } else if (result.actual) {
683 fail(new AssertionError({
684 assertion: 'snapshot',
685 message: message || 'Did not match snapshot',
686 values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})]
687 }));
688 } else {
689 // This can only occur in CI environments.
690 fail(new AssertionError({
691 assertion: 'snapshot',
692 message: message || 'No snapshot available — new snapshots are not created in CI environments'
693 }));
694 }
695 });
696
697 this.truthy = withSkip((actual, message) => {
698 if (!checkMessage('truthy', message)) {
699 return;
700 }
701
702 if (actual) {
703 pass();
704 } else {
705 fail(new AssertionError({
706 assertion: 'truthy',
707 message,
708 operator: '!!',
709 values: [formatWithLabel('Value is not truthy:', actual)]
710 }));
711 }
712 });
713
714 this.falsy = withSkip((actual, message) => {
715 if (!checkMessage('falsy', message)) {
716 return;
717 }
718
719 if (actual) {
720 fail(new AssertionError({
721 assertion: 'falsy',
722 message,
723 operator: '!',
724 values: [formatWithLabel('Value is not falsy:', actual)]
725 }));
726 } else {
727 pass();
728 }
729 });
730
731 this.true = withSkip((actual, message) => {
732 if (!checkMessage('true', message)) {
733 return;
734 }
735
736 if (actual === true) {
737 pass();
738 } else {
739 fail(new AssertionError({
740 assertion: 'true',
741 message,
742 values: [formatWithLabel('Value is not `true`:', actual)]
743 }));
744 }
745 });
746
747 this.false = withSkip((actual, message) => {
748 if (!checkMessage('false', message)) {
749 return;
750 }
751
752 if (actual === false) {
753 pass();
754 } else {
755 fail(new AssertionError({
756 assertion: 'false',
757 message,
758 values: [formatWithLabel('Value is not `false`:', actual)]
759 }));
760 }
761 });
762
763 this.regex = withSkip((string, regex, message) => {
764 if (!checkMessage('regex', message)) {
765 return;
766 }
767
768 if (typeof string !== 'string') {
769 fail(new AssertionError({
770 assertion: 'regex',
771 improperUsage: true,
772 message: '`t.regex()` must be called with a string',
773 values: [formatWithLabel('Called with:', string)]
774 }));
775 return;
776 }
777
778 if (!(regex instanceof RegExp)) {
779 fail(new AssertionError({
780 assertion: 'regex',
781 improperUsage: true,
782 message: '`t.regex()` must be called with a regular expression',
783 values: [formatWithLabel('Called with:', regex)]
784 }));
785 return;
786 }
787
788 if (!regex.test(string)) {
789 fail(new AssertionError({
790 assertion: 'regex',
791 message,
792 values: [
793 formatWithLabel('Value must match expression:', string),
794 formatWithLabel('Regular expression:', regex)
795 ]
796 }));
797 return;
798 }
799
800 pass();
801 });
802
803 this.notRegex = withSkip((string, regex, message) => {
804 if (!checkMessage('notRegex', message)) {
805 return;
806 }
807
808 if (typeof string !== 'string') {
809 fail(new AssertionError({
810 assertion: 'notRegex',
811 improperUsage: true,
812 message: '`t.notRegex()` must be called with a string',
813 values: [formatWithLabel('Called with:', string)]
814 }));
815 return;
816 }
817
818 if (!(regex instanceof RegExp)) {
819 fail(new AssertionError({
820 assertion: 'notRegex',
821 improperUsage: true,
822 message: '`t.notRegex()` must be called with a regular expression',
823 values: [formatWithLabel('Called with:', regex)]
824 }));
825 return;
826 }
827
828 if (regex.test(string)) {
829 fail(new AssertionError({
830 assertion: 'notRegex',
831 message,
832 values: [
833 formatWithLabel('Value must not match expression:', string),
834 formatWithLabel('Regular expression:', regex)
835 ]
836 }));
837 return;
838 }
839
840 pass();
841 });
842
843 if (powerAssert === undefined) {
844 this.assert = withSkip((actual, message) => {
845 if (!checkMessage('assert', message)) {
846 return;
847 }
848
849 if (!actual) {
850 fail(new AssertionError({
851 assertion: 'assert',
852 message,
853 operator: '!!',
854 values: [formatWithLabel('Value is not truthy:', actual)]
855 }));
856 return;
857 }
858
859 pass();
860 });
861 } else {
862 this.assert = withSkip(withPowerAssert(
863 'assert(value, [message])',
864 (actual, message) => {
865 checkMessage('assert', message, true);
866
867 if (!actual) {
868 throw new AssertionError({
869 assertion: 'assert',
870 message,
871 operator: '!!',
872 values: [formatWithLabel('Value is not truthy:', actual)]
873 });
874 }
875 })
876 );
877 }
878 }
879}
880exports.Assertions = Assertions;