UNPKG

41 kBJavaScriptView Raw
1const utils = require('./utils');
2const isRegExp = utils.isRegExp;
3const leftPad = utils.leftPad;
4const arrayChanges = require('array-changes');
5const ukkonen = require('ukkonen');
6const detectIndent = require('detect-indent');
7const defaultDepth = require('./defaultDepth');
8const AssertionString = require('./AssertionString');
9
10module.exports = function(expect) {
11 expect.addType({
12 name: 'wrapperObject',
13 identify: false,
14 equal(a, b, equal) {
15 return a === b || equal(this.unwrap(a), this.unwrap(b));
16 },
17 inspect(value, depth, output, inspect) {
18 output.append(this.prefix(output.clone(), value));
19 output.append(inspect(this.unwrap(value), depth));
20 output.append(this.suffix(output.clone(), value));
21 return output;
22 },
23 diff(actual, expected, output, diff, inspect) {
24 output.inline = true;
25 actual = this.unwrap(actual);
26 expected = this.unwrap(expected);
27 const comparison = diff(actual, expected);
28 const prefixOutput = this.prefix(output.clone(), actual);
29 const suffixOutput = this.suffix(output.clone(), actual);
30 if (comparison && comparison.inline) {
31 return output
32 .append(prefixOutput)
33 .append(comparison)
34 .append(suffixOutput);
35 } else {
36 return output
37 .append(prefixOutput)
38 .nl()
39 .indentLines()
40 .i()
41 .block(function() {
42 this.append(inspect(actual))
43 .sp()
44 .annotationBlock(function() {
45 this.shouldEqualError(expected, inspect);
46 if (comparison) {
47 this.nl(2).append(comparison);
48 }
49 });
50 })
51 .nl()
52 .outdentLines()
53 .append(suffixOutput);
54 }
55 }
56 });
57
58 expect.addType({
59 name: 'Symbol',
60 identify(obj) {
61 return typeof obj === 'symbol';
62 },
63 inspect(obj, depth, output, inspect) {
64 return output
65 .jsKeyword('Symbol')
66 .text('(')
67 .singleQuotedString(obj.toString().replace(/^Symbol\(|\)$/g, ''))
68 .text(')');
69 }
70 });
71
72 expect.addType({
73 name: 'object',
74 indent: true,
75 forceMultipleLines: false,
76 identify(obj) {
77 return obj && typeof obj === 'object';
78 },
79 prefix(output, obj) {
80 const constructor = obj.constructor;
81 const constructorName =
82 constructor &&
83 typeof constructor === 'function' &&
84 constructor !== Object &&
85 utils.getFunctionName(constructor);
86 if (constructorName && constructorName !== 'Object') {
87 output.text(`${constructorName}(`);
88 }
89 return output.text('{');
90 },
91 property(output, key, inspectedValue, isArrayLike) {
92 return output.propertyForObject(key, inspectedValue, isArrayLike);
93 },
94 suffix(output, obj) {
95 output.text('}');
96 const constructor = obj.constructor;
97 const constructorName =
98 constructor &&
99 typeof constructor === 'function' &&
100 constructor !== Object &&
101 utils.getFunctionName(constructor);
102 if (constructorName && constructorName !== 'Object') {
103 output.text(')');
104 }
105 return output;
106 },
107 delimiter(output, i, length) {
108 if (i < length - 1) {
109 output.text(',');
110 }
111 return output;
112 },
113 getKeys: Object.getOwnPropertySymbols
114 ? obj => {
115 const keys = Object.getOwnPropertyNames(obj);
116 const symbols = Object.getOwnPropertySymbols(obj);
117 if (symbols.length > 0) {
118 return keys.concat(symbols);
119 } else {
120 return keys;
121 }
122 }
123 : Object.getOwnPropertyNames,
124 // If Symbol support is not detected default to undefined which, when
125 // passed to Array.prototype.sort, means "natural" (asciibetical) sort.
126 keyComparator:
127 typeof Symbol === 'function'
128 ? (a, b) => {
129 let aString = a;
130 let bString = b;
131 const aIsSymbol = typeof a === 'symbol';
132 const bIsSymbol = typeof b === 'symbol';
133 if (aIsSymbol) {
134 if (bIsSymbol) {
135 aString = a.toString();
136 bString = b.toString();
137 } else {
138 return 1;
139 }
140 } else if (bIsSymbol) {
141 return -1;
142 }
143
144 if (aString < bString) {
145 return -1;
146 } else if (aString > bString) {
147 return 1;
148 }
149
150 return 0;
151 }
152 : undefined,
153 equal(a, b, equal) {
154 return utils.checkObjectEqualityUsingType(a, b, this, equal);
155 },
156 hasKey(obj, key, own) {
157 if (own) {
158 return Object.prototype.hasOwnProperty.call(obj, key);
159 } else {
160 return key in obj;
161 }
162 },
163 inspect(obj, depth, output, inspect) {
164 const keys = this.getKeys(obj);
165 if (keys.length === 0) {
166 this.prefix(output, obj);
167 this.suffix(output, obj);
168 return output;
169 }
170 const type = this;
171 const inspectedItems = keys.map((key, index) => {
172 const propertyDescriptor =
173 Object.getOwnPropertyDescriptor &&
174 Object.getOwnPropertyDescriptor(obj, key);
175 const hasGetter = propertyDescriptor && propertyDescriptor.get;
176 const hasSetter = propertyDescriptor && propertyDescriptor.set;
177 const propertyOutput = output.clone();
178 if (hasSetter && !hasGetter) {
179 propertyOutput.text('set').sp();
180 }
181 // Inspect the setter function if there's no getter:
182 let value;
183 if (hasSetter && !hasGetter) {
184 value = hasSetter;
185 } else {
186 value = type.valueForKey(obj, key);
187 }
188
189 let inspectedValue = inspect(value);
190 if (value && value._expectIt) {
191 inspectedValue = output.clone().block(inspectedValue);
192 }
193 type.property(propertyOutput, key, inspectedValue);
194
195 propertyOutput.amend(
196 type.delimiter(output.clone(), index, keys.length)
197 );
198
199 if (hasGetter && hasSetter) {
200 propertyOutput.sp().jsComment('/* getter/setter */');
201 } else if (hasGetter) {
202 propertyOutput.sp().jsComment('/* getter */');
203 }
204
205 return propertyOutput;
206 });
207
208 const maxLineLength =
209 output.preferredWidth - (depth === Infinity ? 0 : depth) * 2 - 2;
210 let width = 0;
211 const compact =
212 inspectedItems.length > 5 ||
213 inspectedItems.every(inspectedItem => {
214 if (inspectedItem.isMultiline()) {
215 return false;
216 }
217 width += inspectedItem.size().width;
218 return width < maxLineLength;
219 });
220
221 const itemsOutput = output.clone();
222 if (compact) {
223 let currentLineLength = 0;
224 inspectedItems.forEach((inspectedItem, index) => {
225 const size = inspectedItem.size();
226 currentLineLength += size.width + 1;
227 if (index > 0) {
228 if (size.height === 1 && currentLineLength < maxLineLength) {
229 itemsOutput.sp();
230 } else {
231 itemsOutput.nl();
232 currentLineLength = size.width;
233 }
234
235 if (size.height > 1) {
236 // Make sure that we don't append more to this line
237 currentLineLength = maxLineLength;
238 }
239 }
240 itemsOutput.append(inspectedItem);
241 });
242 } else {
243 inspectedItems.forEach((inspectedItem, index) => {
244 if (index > 0) {
245 itemsOutput.nl();
246 }
247 itemsOutput.append(inspectedItem);
248 });
249 }
250
251 const prefixOutput = this.prefix(output.clone(), obj);
252 const suffixOutput = this.suffix(output.clone(), obj);
253 output.append(prefixOutput);
254 if (this.forceMultipleLines || itemsOutput.isMultiline()) {
255 if (!prefixOutput.isEmpty()) {
256 output.nl();
257 }
258 if (this.indent) {
259 output.indentLines().i();
260 }
261 output.block(itemsOutput);
262 if (this.indent) {
263 output.outdentLines();
264 }
265 if (!suffixOutput.isEmpty()) {
266 output.nl();
267 }
268 } else {
269 output
270 .sp(prefixOutput.isEmpty() ? 0 : 1)
271 .append(itemsOutput)
272 .sp(suffixOutput.isEmpty() ? 0 : 1);
273 }
274 return output.append(suffixOutput);
275 },
276 diff(actual, expected, output, diff, inspect, equal) {
277 if (actual.constructor !== expected.constructor) {
278 return output
279 .text('Mismatching constructors ')
280 .text(
281 (actual.constructor && utils.getFunctionName(actual.constructor)) ||
282 actual.constructor
283 )
284 .text(' should be ')
285 .text(
286 (expected.constructor &&
287 utils.getFunctionName(expected.constructor)) ||
288 expected.constructor
289 );
290 }
291
292 output.inline = true;
293 const actualKeys = this.getKeys(actual);
294 const expectedKeys = this.getKeys(expected);
295 const keys = this.uniqueKeys(actualKeys, expectedKeys);
296 const prefixOutput = this.prefix(output.clone(), actual);
297 output.append(prefixOutput).nl(prefixOutput.isEmpty() ? 0 : 1);
298
299 if (this.indent) {
300 output.indentLines();
301 }
302 const type = this;
303 keys.forEach((key, index) => {
304 output
305 .nl(index > 0 ? 1 : 0)
306 .i()
307 .block(function() {
308 const valueActual = type.valueForKey(actual, key);
309 const valueExpected = type.valueForKey(expected, key);
310 const annotation = output.clone();
311 const conflicting = !equal(valueActual, valueExpected);
312 let isInlineDiff = false;
313 let valueOutput;
314 if (conflicting) {
315 if (!type.hasKey(expected, key)) {
316 annotation.error('should be removed');
317 isInlineDiff = true;
318 } else if (!type.hasKey(actual, key)) {
319 this.error('// missing').sp();
320 valueOutput = output.clone().appendInspected(valueExpected);
321 isInlineDiff = true;
322 } else {
323 const keyDiff = diff(valueActual, valueExpected);
324 if (!keyDiff || (keyDiff && !keyDiff.inline)) {
325 annotation.shouldEqualError(valueExpected);
326 if (keyDiff) {
327 annotation.nl(2).append(keyDiff);
328 }
329 } else {
330 isInlineDiff = true;
331 valueOutput = keyDiff;
332 }
333 }
334 } else {
335 isInlineDiff = true;
336 }
337
338 if (!valueOutput) {
339 valueOutput = inspect(valueActual, conflicting ? Infinity : null);
340 }
341
342 valueOutput.amend(
343 type.delimiter(output.clone(), index, actualKeys.length)
344 );
345 if (!isInlineDiff) {
346 valueOutput = output.clone().block(valueOutput);
347 }
348 type.property(this, key, valueOutput);
349 if (!annotation.isEmpty()) {
350 this.sp().annotationBlock(annotation);
351 }
352 });
353 });
354
355 if (this.indent) {
356 output.outdentLines();
357 }
358 const suffixOutput = this.suffix(output.clone(), actual);
359 return output.nl(suffixOutput.isEmpty() ? 0 : 1).append(suffixOutput);
360 },
361 similar(a, b) {
362 if (a === null || b === null) {
363 return false;
364 }
365
366 const typeA = typeof a;
367 const typeB = typeof b;
368
369 if (typeA !== typeB) {
370 return false;
371 }
372
373 if (typeA === 'string') {
374 return ukkonen(a, b) < a.length / 2;
375 }
376
377 if (typeA !== 'object' || !a) {
378 return false;
379 }
380
381 if (utils.isArray(a) && utils.isArray(b)) {
382 return true;
383 }
384
385 const aKeys = this.getKeys(a);
386 const bKeys = this.getKeys(b);
387 let numberOfSimilarKeys = 0;
388 const requiredSimilarKeys = Math.round(
389 Math.max(aKeys.length, bKeys.length) / 2
390 );
391 return aKeys.concat(bKeys).some(key => {
392 if (this.hasKey(a, key) && this.hasKey(b, key)) {
393 numberOfSimilarKeys += 1;
394 }
395 return numberOfSimilarKeys >= requiredSimilarKeys;
396 });
397 },
398 uniqueKeys: utils.uniqueStringsAndSymbols,
399 valueForKey(obj, key) {
400 return obj[key];
401 }
402 });
403
404 expect.addType({
405 name: 'type',
406 base: 'object',
407 identify(value) {
408 return value && value._unexpectedType;
409 },
410 inspect({ name }, depth, output) {
411 return output.text('type: ').jsKeyword(name);
412 }
413 });
414
415 expect.addType({
416 name: 'array-like',
417 base: 'object',
418 identify: false,
419 numericalPropertiesOnly: true,
420 getKeys(obj) {
421 const keys = new Array(obj.length);
422 for (let i = 0; i < obj.length; i += 1) {
423 keys[i] = i;
424 }
425 if (!this.numericalPropertiesOnly) {
426 keys.push(...this.getKeysNonNumerical(obj));
427 }
428 return keys;
429 },
430 getKeysNonNumerical: Object.getOwnPropertySymbols
431 ? obj => {
432 const keys = [];
433 Object.keys(obj).forEach(key => {
434 if (!utils.numericalRegExp.test(key)) {
435 keys.push(key);
436 }
437 });
438 const symbols = Object.getOwnPropertySymbols(obj);
439 if (symbols.length > 0) {
440 keys.push(...symbols);
441 }
442 return keys;
443 }
444 : obj => {
445 const keys = [];
446 Object.keys(obj).forEach(key => {
447 if (!utils.numericalRegExp.test(key)) {
448 keys.push(key);
449 }
450 });
451 return keys;
452 },
453 equal(a, b, equal) {
454 if (a === b) {
455 return true;
456 } else if (a.constructor === b.constructor && a.length === b.length) {
457 let i;
458
459 // compare numerically indexed elements
460 for (i = 0; i < a.length; i += 1) {
461 if (!equal(this.valueForKey(a, i), this.valueForKey(b, i))) {
462 return false;
463 }
464 }
465
466 // compare non-numerical keys if enabled for the type
467 if (!this.numericalPropertiesOnly) {
468 const aKeys = this.getKeysNonNumerical(a).filter(key => {
469 // include keys whose value is not undefined
470 return typeof this.valueForKey(a, key) !== 'undefined';
471 });
472 const bKeys = this.getKeysNonNumerical(b).filter(key => {
473 // include keys whose value is not undefined on either LHS or RHS
474 return (
475 typeof this.valueForKey(b, key) !== 'undefined' ||
476 typeof this.valueForKey(a, key) !== 'undefined'
477 );
478 });
479
480 if (aKeys.length !== bKeys.length) {
481 return false;
482 }
483
484 for (i = 0; i < aKeys.length; i += 1) {
485 if (
486 !equal(
487 this.valueForKey(a, aKeys[i]),
488 this.valueForKey(b, bKeys[i])
489 )
490 ) {
491 return false;
492 }
493 }
494 }
495
496 return true;
497 } else {
498 return false;
499 }
500 },
501 prefix(output) {
502 return output.text('[');
503 },
504 suffix(output) {
505 return output.text(']');
506 },
507 inspect(arr, depth, output, inspect) {
508 const prefixOutput = this.prefix(output.clone(), arr);
509 const suffixOutput = this.suffix(output.clone(), arr);
510 const keys = this.getKeys(arr);
511 if (keys.length === 0) {
512 return output.append(prefixOutput).append(suffixOutput);
513 }
514
515 if (depth === 1 && arr.length > 10) {
516 return output
517 .append(prefixOutput)
518 .text('...')
519 .append(suffixOutput);
520 }
521
522 const inspectedItems = keys.map(key => {
523 let inspectedValue;
524 if (this.hasKey(arr, key)) {
525 inspectedValue = inspect(this.valueForKey(arr, key));
526 } else if (utils.numericalRegExp.test(key)) {
527 // Sparse array entry
528 inspectedValue = output.clone();
529 } else {
530 // Not present non-numerical property returned by getKeys
531 inspectedValue = inspect(undefined);
532 }
533 return this.property(output.clone(), key, inspectedValue, true);
534 });
535
536 const currentDepth = defaultDepth - Math.min(defaultDepth, depth);
537 const maxLineLength =
538 output.preferredWidth - 20 - currentDepth * output.indentationWidth - 2;
539 let width = 0;
540 const multipleLines =
541 this.forceMultipleLines ||
542 inspectedItems.some(o => {
543 if (o.isMultiline()) {
544 return true;
545 }
546
547 const size = o.size();
548 width += size.width;
549 return width > maxLineLength;
550 });
551 inspectedItems.forEach((inspectedItem, index) => {
552 inspectedItem.amend(this.delimiter(output.clone(), index, keys.length));
553 });
554 if (multipleLines) {
555 output.append(prefixOutput);
556 if (!prefixOutput.isEmpty()) {
557 output.nl();
558 }
559 if (this.indent) {
560 output.indentLines();
561 }
562 inspectedItems.forEach((inspectedItem, index) => {
563 output
564 .nl(index > 0 ? 1 : 0)
565 .i()
566 .block(inspectedItem);
567 });
568
569 if (this.indent) {
570 output.outdentLines();
571 }
572
573 if (!suffixOutput.isEmpty()) {
574 output.nl();
575 }
576
577 return output.append(suffixOutput);
578 } else {
579 output.append(prefixOutput).sp(prefixOutput.isEmpty() ? 0 : 1);
580 inspectedItems.forEach((inspectedItem, index) => {
581 output.append(inspectedItem);
582 const lastIndex = index === inspectedItems.length - 1;
583 if (!lastIndex) {
584 output.sp();
585 }
586 });
587 return output.sp(suffixOutput.isEmpty() ? 0 : 1).append(suffixOutput);
588 }
589 },
590 diffLimit: 512,
591 diff(actual, expected, output, diff, inspect, equal) {
592 output.inline = true;
593
594 if (Math.max(actual.length, expected.length) > this.diffLimit) {
595 output.jsComment(`Diff suppressed due to size > ${this.diffLimit}`);
596 return output;
597 }
598
599 if (actual.constructor !== expected.constructor) {
600 return this.baseType.diff(actual, expected, output);
601 }
602
603 const prefixOutput = this.prefix(output.clone(), actual);
604 output.append(prefixOutput).nl(prefixOutput.isEmpty() ? 0 : 1);
605
606 if (this.indent) {
607 output.indentLines();
608 }
609
610 var actualElements = utils.duplicateArrayLikeUsingType(actual, this);
611 var actualKeys = this.getKeys(actual);
612 var expectedElements = utils.duplicateArrayLikeUsingType(expected, this);
613 var expectedKeys = this.getKeys(expected);
614 var nonNumericalKeysAndSymbols =
615 !this.numericalPropertiesOnly &&
616 utils.uniqueNonNumericalStringsAndSymbols(actualKeys, expectedKeys);
617
618 const type = this;
619 const changes = arrayChanges(
620 actualElements,
621 expectedElements,
622 equal,
623 (a, b) => type.similar(a, b),
624 {
625 includeNonNumericalProperties: nonNumericalKeysAndSymbols
626 }
627 );
628 const indexOfLastNonInsert = changes.reduce(
629 (previousValue, diffItem, index) =>
630 diffItem.type === 'insert' ? previousValue : index,
631 -1
632 );
633 const packing = utils.packArrows(changes); // NOTE: Will have side effects in changes if the packing results in too many arrow lanes
634 output.arrowsAlongsideChangeOutputs(
635 packing,
636 changes.map((diffItem, index) => {
637 const delimiterOutput = type.delimiter(
638 output.clone(),
639 index,
640 indexOfLastNonInsert + 1
641 );
642 if (diffItem.type === 'moveTarget') {
643 return output.clone();
644 } else {
645 return output.clone().block(function() {
646 if (diffItem.type === 'moveSource') {
647 const propertyOutput = type.property(
648 output.clone(),
649 diffItem.actualIndex,
650 inspect(diffItem.value),
651 true
652 );
653 this.amend(propertyOutput)
654 .amend(delimiterOutput)
655 .sp()
656 .error('// should be moved');
657 } else if (diffItem.type === 'insert') {
658 this.annotationBlock(function() {
659 this.error('missing ').block(function() {
660 const index =
661 typeof diffItem.actualIndex !== 'undefined'
662 ? diffItem.actualIndex
663 : diffItem.expectedIndex;
664 const propertyOutput = type.property(
665 output.clone(),
666 index,
667 inspect(diffItem.value),
668 true
669 );
670 this.amend(propertyOutput);
671 });
672 });
673 } else if (diffItem.type === 'remove') {
674 this.block(function() {
675 const propertyOutput = type.property(
676 output.clone(),
677 diffItem.actualIndex,
678 inspect(diffItem.value),
679 true
680 );
681 this.amend(propertyOutput)
682 .amend(delimiterOutput)
683 .sp()
684 .error('// should be removed');
685 });
686 } else if (diffItem.type === 'equal') {
687 this.block(function() {
688 const propertyOutput = type.property(
689 output.clone(),
690 diffItem.actualIndex,
691 inspect(diffItem.value),
692 true
693 );
694 this.amend(propertyOutput).amend(delimiterOutput);
695 });
696 } else {
697 this.block(function() {
698 const valueDiff = diff(diffItem.value, diffItem.expected);
699 if (valueDiff && valueDiff.inline) {
700 this.append(valueDiff).append(delimiterOutput);
701 } else {
702 const propertyOutput = type.property(
703 output.clone(),
704 diffItem.actualIndex,
705 inspect(diffItem.value),
706 true
707 );
708 this.append(propertyOutput)
709 .append(delimiterOutput)
710 .sp()
711 .annotationBlock(function() {
712 this.shouldEqualError(diffItem.expected, inspect);
713 if (valueDiff) {
714 this.nl(2).append(valueDiff);
715 }
716 });
717 }
718 });
719 }
720 });
721 }
722 })
723 );
724
725 if (this.indent) {
726 output.outdentLines();
727 }
728
729 const suffixOutput = this.suffix(output.clone(), actual);
730 return output.nl(suffixOutput.isEmpty() ? 0 : 1).append(suffixOutput);
731 }
732 });
733
734 expect.addType({
735 name: 'array',
736 base: 'array-like',
737 numericalPropertiesOnly: false,
738 identify(arr) {
739 return utils.isArray(arr);
740 }
741 });
742
743 expect.addType({
744 name: 'arguments',
745 base: 'array-like',
746 prefix(output) {
747 return output.text('arguments(', 'cyan');
748 },
749 suffix(output) {
750 return output.text(')', 'cyan');
751 },
752 identify(obj) {
753 return Object.prototype.toString.call(obj) === '[object Arguments]';
754 }
755 });
756
757 const errorMethodBlacklist = [
758 'message',
759 'name',
760 'description',
761 'line',
762 'number',
763 'column',
764 'sourceId',
765 'sourceURL',
766 'stack',
767 'stackArray',
768 '__stackCleaned__',
769 'isOperational' // added by the promise implementation
770 ].reduce((result, prop) => {
771 result[prop] = true;
772 return result;
773 }, {});
774
775 if (Object.prototype.hasOwnProperty.call(new Error(), 'arguments')) {
776 // node.js 0.10 adds two extra non-enumerable properties to Error instances:
777 errorMethodBlacklist.arguments = true;
778 errorMethodBlacklist.type = true;
779 }
780
781 expect.addType({
782 base: 'object',
783 name: 'Error',
784 identify(value) {
785 return utils.isError(value);
786 },
787 getKeys(value) {
788 const keys = this.baseType
789 .getKeys(value)
790 .filter(key => !errorMethodBlacklist[key]);
791 keys.unshift('message');
792 return keys;
793 },
794 unwrap(value) {
795 return this.getKeys(value).reduce((result, key) => {
796 result[key] = value[key];
797 return result;
798 }, {});
799 },
800 equal(a, b, equal) {
801 return (
802 a === b ||
803 (equal(a.message, b.message) &&
804 utils.checkObjectEqualityUsingType(a, b, this, equal))
805 );
806 },
807 inspect(value, depth, output, inspect) {
808 output.errorName(value).text('(');
809 const keys = this.getKeys(value);
810 if (keys.length === 1 && keys[0] === 'message') {
811 if (value.message !== '') {
812 output.append(inspect(value.message));
813 }
814 } else {
815 output.append(inspect(this.unwrap(value), depth));
816 }
817 return output.text(')');
818 },
819 diff(actual, expected, output, diff) {
820 if (actual.constructor !== expected.constructor) {
821 return output
822 .text('Mismatching constructors ')
823 .errorName(actual)
824 .text(' should be ')
825 .errorName(expected);
826 }
827
828 output = diff(this.unwrap(actual), this.unwrap(expected));
829 if (output) {
830 output = output
831 .clone()
832 .errorName(actual)
833 .text('(')
834 .append(output)
835 .text(')');
836 output.inline = false;
837 }
838 return output;
839 }
840 });
841
842 const unexpectedErrorMethodBlacklist = [
843 'output',
844 '_isUnexpected',
845 'htmlMessage',
846 '_hasSerializedErrorMessage',
847 'expect',
848 'assertion',
849 'originalError'
850 ].reduce((result, prop) => {
851 result[prop] = true;
852 return result;
853 }, {});
854 expect.addType({
855 base: 'Error',
856 name: 'UnexpectedError',
857 identify(value) {
858 return (
859 value &&
860 typeof value === 'object' &&
861 value._isUnexpected &&
862 this.baseType.identify(value)
863 );
864 },
865 getKeys(value) {
866 return this.baseType
867 .getKeys(value)
868 .filter(key => !unexpectedErrorMethodBlacklist[key]);
869 },
870 inspect(value, depth, output) {
871 output.jsFunctionName(this.name).text('(');
872 const errorMessage = value.getErrorMessage(output);
873 if (errorMessage.isMultiline()) {
874 output
875 .nl()
876 .indentLines()
877 .i()
878 .block(errorMessage)
879 .nl();
880 } else {
881 output.append(errorMessage);
882 }
883 return output.text(')');
884 }
885 });
886
887 expect.addType({
888 name: 'date',
889 identify(obj) {
890 return Object.prototype.toString.call(obj) === '[object Date]';
891 },
892 equal(a, b) {
893 return a.getTime() === b.getTime();
894 },
895 inspect(date, depth, output, inspect) {
896 // TODO: Inspect "new" as an operator and Date as a built-in once we have the styles defined:
897 let dateStr = date.toUTCString().replace(/UTC/, 'GMT');
898 const milliseconds = date.getUTCMilliseconds();
899 if (milliseconds > 0) {
900 let millisecondsStr = String(milliseconds);
901 while (millisecondsStr.length < 3) {
902 millisecondsStr = `0${millisecondsStr}`;
903 }
904 dateStr = dateStr.replace(' GMT', `.${millisecondsStr} GMT`);
905 }
906
907 return output
908 .jsKeyword('new')
909 .sp()
910 .text('Date(')
911 .append(inspect(dateStr).text(')'));
912 }
913 });
914
915 expect.addType({
916 base: 'object',
917 name: 'function',
918 identify(f) {
919 return typeof f === 'function';
920 },
921 getKeys: Object.keys,
922 equal(a, b) {
923 return a === b;
924 },
925 inspect(f, depth, output, inspect) {
926 // Don't break when a function has its own custom #toString:
927 const source = Function.prototype.toString
928 .call(f)
929 .replace(/\r\n?|\n\r?/g, '\n');
930 let name = utils.getFunctionName(f) || '';
931 let preamble;
932 let body;
933 let bodyIndent;
934 const matchSource = source.match(
935 /^\s*((?:async )?\s*(?:\S+\s*=>|\([^)]*\)\s*=>|class|function\s?(?:\*\s*)?\w*\s*\([^)]*\)))([\s\S]*)$/
936 );
937 if (matchSource) {
938 // Normalize so there's always space after "function" and never after "*" for generator functions:
939 preamble = matchSource[1]
940 .replace(/function(\S)/, 'function $1')
941 .replace(/\* /, '*');
942 if (preamble === 'function ()' && name) {
943 // fn.bind() doesn't seem to include the name in the .toString() output:
944 preamble = `function ${name}()`;
945 }
946 body = matchSource[2];
947 let matchBodyAndIndent = body.match(/^(\s*\{)([\s\S]*?)([ ]*)\}\s*$/);
948 let openingBrace;
949 let isWrappedInBraces = true;
950 let closingBrace = '}';
951 let reindentBodyLevel = 0;
952 if (matchBodyAndIndent) {
953 openingBrace = matchBodyAndIndent[1];
954 body = matchBodyAndIndent[2];
955 bodyIndent = matchBodyAndIndent[3] || '';
956 if (bodyIndent.length === 1) {
957 closingBrace = ' }';
958 }
959 } else {
960 // Attempt to match an arrow function with an implicit return body.
961 matchBodyAndIndent = body.match(/^(\s*)([\s\S]*?)([ ]*)\s*$/);
962
963 if (matchBodyAndIndent) {
964 openingBrace = matchBodyAndIndent[1];
965 isWrappedInBraces = false;
966 body = matchBodyAndIndent[2];
967 const matchInitialNewline = openingBrace.match(/^\n( +)/);
968 if (matchInitialNewline) {
969 openingBrace = '\n';
970 if (/\n/.test(body)) {
971 // An arrow function whose body starts with a newline, as prettier likes to output, eg.:
972 // () =>
973 // foo(
974 // 1
975 // );
976 // Shuffle/hack things around so it will be formatted correctly:
977 bodyIndent = matchInitialNewline[1];
978 reindentBodyLevel = 1;
979 } else {
980 body = body.replace(/^\s*/, ' ');
981 }
982 } else {
983 bodyIndent = matchBodyAndIndent[3] || '';
984 }
985 closingBrace = '';
986 }
987 }
988
989 // Remove leading indentation unless the function is a one-liner or it uses multiline string literals
990 if (/\n/.test(body) && !/\\\n/.test(body)) {
991 body = body.replace(new RegExp(`^ {${bodyIndent.length}}`, 'mg'), '');
992 const indent = detectIndent(body);
993 body = body.replace(
994 new RegExp(`^(?:${indent.indent})*`, 'mg'),
995 ({ length }) =>
996 utils.leftPad(
997 '',
998 (length / indent.amount + reindentBodyLevel) *
999 output.indentationWidth,
1000 ' '
1001 )
1002 );
1003 }
1004 if (!name || name === 'anonymous') {
1005 name = '';
1006 }
1007 if (/^\s*\[native code\]\s*$/.test(body)) {
1008 body = ' /* native code */ ';
1009 closingBrace = '}';
1010 } else if (/^\s*$/.test(body)) {
1011 body = '';
1012 } else if (
1013 /^\s*[^\r\n]{1,30}\s*$/.test(body) &&
1014 body.indexOf('//') === -1 &&
1015 isWrappedInBraces
1016 ) {
1017 body = ` ${body.trim()} `;
1018 closingBrace = '}';
1019 } else {
1020 body = body.replace(
1021 /^((?:.*\n){3}( *).*\n)[\s\S]*?\n[\s\S]*?\n((?:.*\n){3})$/,
1022 '$1$2// ... lines removed ...\n$3'
1023 );
1024 }
1025 if (matchBodyAndIndent) {
1026 body = openingBrace + body + closingBrace;
1027 } else {
1028 // Strip trailing space from arrow function body
1029 body = body.replace(/[ ]*$/, '');
1030 }
1031 } else {
1032 preamble = `function ${name}( /*...*/ ) `;
1033 body = '{ /*...*/ }';
1034 }
1035 return output.code(preamble + body, 'javascript');
1036 },
1037 diff(actual, expected, output) {
1038 // Avoid rendering an object diff when both are functions
1039 if (typeof actual !== 'function' || typeof expected !== 'function') {
1040 return this.baseType.diff(actual, expected, output);
1041 }
1042 }
1043 });
1044
1045 expect.addType({
1046 base: 'function',
1047 name: 'expect.it',
1048 identify(f) {
1049 return typeof f === 'function' && f._expectIt;
1050 },
1051 inspect({ _expectations, _OR }, depth, output, inspect) {
1052 output.text('expect.it(');
1053 let orBranch = false;
1054 _expectations.forEach((expectation, index) => {
1055 if (expectation === _OR) {
1056 orBranch = true;
1057 return;
1058 }
1059
1060 if (orBranch) {
1061 output.text(')\n .or(');
1062 } else if (index > 0) {
1063 output.text(')\n .and(');
1064 }
1065
1066 const args = Array.prototype.slice.call(expectation);
1067 args.forEach((arg, i) => {
1068 if (i > 0) {
1069 output.text(', ');
1070 }
1071 output.append(inspect(arg));
1072 });
1073 orBranch = false;
1074 });
1075
1076 return output.amend(')');
1077 }
1078 });
1079
1080 expect.addType({
1081 name: 'Promise',
1082 base: 'object',
1083 identify(obj) {
1084 return (
1085 obj && this.baseType.identify(obj) && typeof obj.then === 'function'
1086 );
1087 },
1088 inspect(promise, depth, output, inspect) {
1089 output.jsFunctionName('Promise');
1090 if (promise.isPending && promise.isPending()) {
1091 output.sp().yellow('(pending)');
1092 } else if (promise.isFulfilled && promise.isFulfilled()) {
1093 output.sp().green('(fulfilled)');
1094 if (promise.value) {
1095 const value = promise.value();
1096 if (typeof value !== 'undefined') {
1097 output
1098 .sp()
1099 .text('=>')
1100 .sp()
1101 .append(inspect(value));
1102 }
1103 }
1104 } else if (promise.isRejected && promise.isRejected()) {
1105 output.sp().red('(rejected)');
1106 const reason = promise.reason();
1107 if (typeof reason !== 'undefined') {
1108 output
1109 .sp()
1110 .text('=>')
1111 .sp()
1112 .append(inspect(promise.reason()));
1113 }
1114 }
1115 return output;
1116 }
1117 });
1118
1119 expect.addType({
1120 name: 'regexp',
1121 base: 'object',
1122 identify: isRegExp,
1123 equal(a, b) {
1124 return (
1125 a === b ||
1126 (a.source === b.source &&
1127 a.global === b.global &&
1128 a.ignoreCase === b.ignoreCase &&
1129 a.multiline === b.multiline)
1130 );
1131 },
1132 inspect(regExp, depth, output) {
1133 return output.jsRegexp(regExp);
1134 },
1135 diff(actual, expected, output, diff, inspect) {
1136 output.inline = false;
1137 return output.stringDiff(String(actual), String(expected), {
1138 type: 'Chars',
1139 markUpSpecialCharacters: true
1140 });
1141 }
1142 });
1143
1144 expect.addType({
1145 name: 'binaryArray',
1146 base: 'array-like',
1147 digitWidth: 2,
1148 hexDumpWidth: 16,
1149 identify: false,
1150 prefix(output) {
1151 return output.code(`${this.name}([`, 'javascript');
1152 },
1153 suffix(output) {
1154 return output.code('])', 'javascript');
1155 },
1156 equal(a, b) {
1157 if (a === b) {
1158 return true;
1159 }
1160
1161 if (a.length !== b.length) {
1162 return false;
1163 }
1164
1165 for (let i = 0; i < a.length; i += 1) {
1166 if (a[i] !== b[i]) {
1167 return false;
1168 }
1169 }
1170
1171 return true;
1172 },
1173 hexDump(obj, maxLength) {
1174 let hexDump = '';
1175 if (typeof maxLength !== 'number' || maxLength === 0) {
1176 maxLength = obj.length;
1177 }
1178 for (let i = 0; i < maxLength; i += this.hexDumpWidth) {
1179 if (hexDump.length > 0) {
1180 hexDump += '\n';
1181 }
1182 let hexChars = '';
1183 let asciiChars = ' │';
1184
1185 for (let j = 0; j < this.hexDumpWidth; j += 1) {
1186 if (i + j < maxLength) {
1187 const octet = obj[i + j];
1188 hexChars += `${leftPad(
1189 octet.toString(16).toUpperCase(),
1190 this.digitWidth,
1191 '0'
1192 )} `;
1193 asciiChars += String.fromCharCode(octet)
1194 .replace(/\n/g, '␊')
1195 .replace(/\r/g, '␍');
1196 } else if (this.digitWidth === 2) {
1197 hexChars += ' ';
1198 }
1199 }
1200
1201 if (this.digitWidth === 2) {
1202 hexDump += `${hexChars + asciiChars}│`;
1203 } else {
1204 hexDump += hexChars.replace(/\s+$/, '');
1205 }
1206 }
1207 return hexDump;
1208 },
1209 inspect(obj, depth, output) {
1210 this.prefix(output, obj);
1211 let codeStr = '';
1212 for (let i = 0; i < Math.min(this.hexDumpWidth, obj.length); i += 1) {
1213 if (i > 0) {
1214 codeStr += ', ';
1215 }
1216 const octet = obj[i];
1217 codeStr += `0x${leftPad(
1218 octet.toString(16).toUpperCase(),
1219 this.digitWidth,
1220 '0'
1221 )}`;
1222 }
1223 if (obj.length > this.hexDumpWidth) {
1224 codeStr += ` /* ${obj.length - this.hexDumpWidth} more */ `;
1225 }
1226 output.code(codeStr, 'javascript');
1227 this.suffix(output, obj);
1228 return output;
1229 },
1230 diffLimit: 512,
1231 diff(actual, expected, output, diff, inspect) {
1232 output.inline = false;
1233 if (Math.max(actual.length, expected.length) > this.diffLimit) {
1234 output.jsComment(`Diff suppressed due to size > ${this.diffLimit}`);
1235 } else {
1236 output
1237 .stringDiff(this.hexDump(actual), this.hexDump(expected), {
1238 type: 'Chars',
1239 markUpSpecialCharacters: false
1240 })
1241 // eslint-disable-next-line no-control-regex
1242 .replaceText(/[\x00-\x1f\x7f-\xff␊␍]/g, '.')
1243 .replaceText(/[│ ]/g, function(styles, content) {
1244 this.text(content);
1245 });
1246 }
1247 return output;
1248 }
1249 });
1250
1251 [8, 16, 32].forEach(function(numBits) {
1252 ['Int', 'Uint'].forEach(intOrUint => {
1253 const constructorName = `${intOrUint + numBits}Array`;
1254 const Constructor = global[constructorName];
1255 expect.addType({
1256 name: constructorName,
1257 base: 'binaryArray',
1258 hexDumpWidth: 128 / numBits,
1259 digitWidth: numBits / 4,
1260 identify:
1261 Constructor !== 'undefined' &&
1262 function(obj) {
1263 return obj instanceof Constructor;
1264 }
1265 });
1266 }, this);
1267 }, this);
1268
1269 expect.addType({
1270 name: 'Buffer',
1271 base: 'binaryArray',
1272 identify: typeof Buffer === 'function' && Buffer.isBuffer,
1273 prefix(output) {
1274 return output.code(`Buffer.from([`, 'javascript');
1275 }
1276 });
1277
1278 expect.addType({
1279 name: 'string',
1280 identify(value) {
1281 return typeof value === 'string';
1282 },
1283 inspect(value, depth, output) {
1284 return output.singleQuotedString(value);
1285 },
1286 diffLimit: 65536,
1287 diff(actual, expected, output, diff, inspect) {
1288 if (Math.max(actual.length, expected.length) > this.diffLimit) {
1289 output.jsComment(`Diff suppressed due to size > ${this.diffLimit}`);
1290 return output;
1291 }
1292 output.stringDiff(actual, expected, {
1293 type: 'WordsWithSpace',
1294 markUpSpecialCharacters: true
1295 });
1296 output.inline = false;
1297 return output;
1298 }
1299 });
1300
1301 expect.addType({
1302 name: 'number',
1303 identify(value) {
1304 return typeof value === 'number' && !isNaN(value);
1305 },
1306 inspect(value, depth, output) {
1307 if (value === 0 && 1 / value === -Infinity) {
1308 value = '-0';
1309 } else {
1310 value = String(value);
1311 }
1312 return output.jsNumber(String(value));
1313 }
1314 });
1315
1316 expect.addType({
1317 name: 'NaN',
1318 identify(value) {
1319 return typeof value === 'number' && isNaN(value);
1320 },
1321 inspect(value, depth, output) {
1322 return output.jsPrimitive(value);
1323 }
1324 });
1325
1326 expect.addType({
1327 name: 'BigInt',
1328 identify(value) {
1329 return typeof value === 'bigint';
1330 },
1331 inspect(value, depth, output) {
1332 return output
1333 .code('BigInt(', 'javascript')
1334 .jsNumber(value.toString())
1335 .code(')', 'javascript');
1336 }
1337 });
1338
1339 expect.addType({
1340 name: 'boolean',
1341 identify(value) {
1342 return typeof value === 'boolean';
1343 },
1344 inspect(value, depth, output) {
1345 return output.jsPrimitive(value);
1346 }
1347 });
1348
1349 expect.addType({
1350 name: 'undefined',
1351 identify(value) {
1352 return typeof value === 'undefined';
1353 },
1354 inspect(value, depth, output) {
1355 return output.jsPrimitive(value);
1356 }
1357 });
1358
1359 expect.addType({
1360 name: 'null',
1361 identify(value) {
1362 return value === null;
1363 },
1364 inspect(value, depth, output) {
1365 return output.jsPrimitive(value);
1366 }
1367 });
1368
1369 expect.addType({
1370 name: 'assertion',
1371 identify(value) {
1372 return value instanceof AssertionString;
1373 }
1374 });
1375};