strong-mock 8.0.1 → 9.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +52 -30
  2. package/dist/errors/api.d.ts +13 -0
  3. package/dist/errors/unexpected-access.d.ts +5 -0
  4. package/dist/errors/unexpected-call.d.ts +17 -0
  5. package/dist/errors/verify.d.ts +8 -0
  6. package/dist/expectation/expectation.d.ts +27 -30
  7. package/dist/expectation/repository/expectation-repository.d.ts +90 -90
  8. package/dist/expectation/repository/flexible-repository.d.ts +38 -38
  9. package/dist/expectation/repository/return-value.d.ts +13 -13
  10. package/dist/expectation/strong-expectation.d.ts +30 -30
  11. package/dist/index.d.ts +14 -9
  12. package/dist/index.js +810 -512
  13. package/dist/index.js.map +1 -1
  14. package/dist/matchers/deep-equals.d.ts +16 -0
  15. package/dist/matchers/is-any.d.ts +11 -0
  16. package/dist/matchers/is-array.d.ts +22 -0
  17. package/dist/matchers/is-number.d.ts +12 -0
  18. package/dist/matchers/is-object.d.ts +27 -0
  19. package/dist/matchers/is-string.d.ts +15 -0
  20. package/dist/matchers/is.d.ts +9 -0
  21. package/dist/matchers/it.d.ts +9 -0
  22. package/dist/matchers/matcher.d.ts +92 -0
  23. package/dist/matchers/will-capture.d.ts +21 -0
  24. package/dist/mock/defaults.d.ts +11 -11
  25. package/dist/mock/map.d.ts +16 -16
  26. package/dist/mock/mock.d.ts +29 -29
  27. package/dist/mock/options.d.ts +99 -99
  28. package/dist/mock/stub.d.ts +5 -5
  29. package/dist/print.d.ts +10 -10
  30. package/dist/proxy.d.ts +48 -48
  31. package/dist/return/invocation-count.d.ts +44 -44
  32. package/dist/return/returns.d.ts +61 -87
  33. package/dist/verify/reset.d.ts +20 -20
  34. package/dist/verify/verify.d.ts +27 -27
  35. package/dist/when/{pending-expectation.d.ts → expectation-builder.d.ts} +26 -31
  36. package/dist/when/when.d.ts +32 -32
  37. package/package.json +20 -16
  38. package/dist/errors.d.ts +0 -28
  39. package/dist/expectation/it.d.ts +0 -29
  40. package/dist/expectation/matcher.d.ts +0 -21
package/dist/index.js CHANGED
@@ -1,19 +1,84 @@
1
1
  var jestMatcherUtils = require('jest-matcher-utils');
2
+ var jestDiff = require('jest-diff');
3
+ var stripAnsi = require('strip-ansi');
2
4
  var lodash = require('lodash');
3
5
 
6
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
7
+
8
+ var stripAnsi__default = /*#__PURE__*/_interopDefaultLegacy(stripAnsi);
9
+
10
+ /**
11
+ * Special symbol denoting the call of a function.
12
+ */
13
+ const ApplyProp = Symbol('apply');
14
+
4
15
  const MATCHER_SYMBOL = Symbol('matcher');
5
- /**
6
- * Used to test if an expectation on an argument is a custom matcher.
16
+ /**
17
+ * Used to test if an expectation is an argument is a custom matcher.
7
18
  */
8
19
 
9
20
  function isMatcher(f) {
10
21
  return !!(f && f[MATCHER_SYMBOL]);
11
22
  }
12
-
13
- /**
14
- * Special symbol denoting the call of a function.
23
+ const getMatcherDiffs = (matchers, args) => {
24
+ const matcherDiffs = matchers.map((matcher, i) => matcher.getDiff(args[i]));
25
+ const actual = matcherDiffs.map(d => d.actual);
26
+ const expected = matcherDiffs.map(d => d.expected);
27
+ return {
28
+ actual,
29
+ expected
30
+ };
31
+ };
32
+ /**
33
+ * Create a custom matcher.
34
+ *
35
+ * @param predicate Will receive the actual value and return whether it matches the expectation.
36
+ * @param options
37
+ * @param options.toString An optional function that should return a string that will be
38
+ * used when the matcher needs to be printed in an error message. By default,
39
+ * it stringifies `predicate`.
40
+ * @param options.getDiff An optional function that will be called when printing the
41
+ * diff for a failed expectation. It will only be called if there's a mismatch
42
+ * between the expected and received values i.e. `predicate(actual)` fails.
43
+ * By default, the `toString` method will be used to format the expected value,
44
+ * while the received value will be returned as-is.
45
+ *
46
+ * @example
47
+ * // Create a matcher for positive numbers.
48
+ * const fn = mock<(x: number) => number>();
49
+ * when(() => fn(It.matches(x => x >= 0))).thenReturn(42);
50
+ *
51
+ * fn(2) === 42
52
+ * fn(-1) // throws
15
53
  */
16
- const ApplyProp = Symbol('apply');
54
+
55
+ const matches = (predicate, options) => {
56
+ var _options$toString, _options$getDiff;
57
+
58
+ // We can't use destructuring with default values because `options` is optional,
59
+ // so it needs a default value of `{}`, which will come with a native `toString`.
60
+ const toString = (_options$toString = options == null ? void 0 : options.toString) != null ? _options$toString : () => `Matcher(${predicate.toString()})`;
61
+ const getDiff = (_options$getDiff = options == null ? void 0 : options.getDiff) != null ? _options$getDiff : actual => ({
62
+ actual,
63
+ expected: toString()
64
+ });
65
+ const matcher = {
66
+ [MATCHER_SYMBOL]: true,
67
+ matches: actual => predicate(actual),
68
+ toString,
69
+ getDiff: actual => {
70
+ if (predicate(actual)) {
71
+ return {
72
+ actual,
73
+ expected: actual
74
+ };
75
+ }
76
+
77
+ return getDiff(actual);
78
+ }
79
+ };
80
+ return matcher;
81
+ };
17
82
 
18
83
  const printProperty = property => {
19
84
  if (property === ApplyProp) {
@@ -26,12 +91,26 @@ const printProperty = property => {
26
91
 
27
92
  return `.${property}`;
28
93
  };
29
- const printArg = arg => // Call toJSON on matchers directly to avoid wrapping them in quotes.
30
- isMatcher(arg) ? arg.toJSON() : jestMatcherUtils.printExpected(arg);
94
+ const printValue = arg => {
95
+ // Call toString on matchers directly to avoid wrapping strings returned by them in quotes.
96
+ if (isMatcher(arg)) {
97
+ return arg.toString();
98
+ }
99
+
100
+ return jestMatcherUtils.stringify(arg);
101
+ };
102
+
103
+ const printArgs = args => args.map(arg => printValue(arg)).join(', ');
104
+
31
105
  const printCall = (property, args) => {
32
- const prettyArgs = args.map(arg => printArg(arg)).join(', ');
33
106
  const prettyProperty = printProperty(property);
34
- return `${prettyProperty}(${prettyArgs})`;
107
+
108
+ if (args) {
109
+ const prettyArgs = printArgs(args);
110
+ return `mock${jestMatcherUtils.RECEIVED_COLOR(`${prettyProperty}(${prettyArgs})`)}`;
111
+ }
112
+
113
+ return `mock${jestMatcherUtils.RECEIVED_COLOR(`${prettyProperty}`)}`;
35
114
  };
36
115
  const printReturns = ({
37
116
  isError,
@@ -52,118 +131,141 @@ const printReturns = ({
52
131
  thenPrefix += 'thenReturn';
53
132
  }
54
133
 
55
- return `.${thenPrefix}(${jestMatcherUtils.printExpected(value)}).between(${min}, ${max})`;
134
+ return `.${thenPrefix}(${jestMatcherUtils.RECEIVED_COLOR(printValue(value))}).between(${min}, ${max})`;
56
135
  };
57
136
  const printWhen = (property, args) => {
137
+ const prettyProperty = printProperty(property);
138
+
58
139
  if (args) {
59
- return `when(() => ${jestMatcherUtils.EXPECTED_COLOR(`mock${printCall(property, args)}`)})`;
140
+ return `when(() => mock${jestMatcherUtils.EXPECTED_COLOR(`${prettyProperty}(${printArgs(args)})`)})`;
60
141
  }
61
142
 
62
- return `when(() => ${jestMatcherUtils.EXPECTED_COLOR(`mock${printProperty(property)}`)})`;
143
+ return `when(() => mock${jestMatcherUtils.EXPECTED_COLOR(`${printProperty(property)}`)})`;
63
144
  };
64
145
  const printExpectation = (property, args, returnValue, min, max) => `${printWhen(property, args)}${printReturns(returnValue, min, max)}`;
65
146
  const printRemainingExpectations = expectations => expectations.length ? `Remaining unmet expectations:
66
- - ${expectations.map(e => e.toJSON()).join('\n - ')}` : 'There are no remaining unmet expectations.';
67
-
68
- class UnfinishedExpectation extends Error {
69
- constructor(pendingExpectation) {
70
- super(`There is an unfinished pending expectation:
71
-
72
- ${pendingExpectation.toJSON()}
73
-
74
- Please finish it by setting a return value even if the value
75
- is undefined.`);
76
- }
147
+ - ${expectations.map(e => e.toString()).join('\n - ')}` : 'There are no remaining unmet expectations.';
77
148
 
78
- }
79
- class MissingWhen extends Error {
80
- constructor() {
81
- super(`You tried setting a return value without an expectation.
82
-
83
- Every call to set a return value must be preceded by an expectation.`);
84
- }
85
-
86
- }
87
149
  class UnexpectedAccess extends Error {
88
150
  constructor(property, expectations) {
89
- super(`Didn't expect ${jestMatcherUtils.EXPECTED_COLOR(`mock${printProperty(property)}`)} to be accessed.
151
+ super(jestMatcherUtils.DIM_COLOR(`Didn't expect ${printCall(property)} to be accessed.
90
152
 
91
153
  If you expect this property to be accessed then please
92
154
  set an expectation for it.
93
155
 
94
- ${printRemainingExpectations(expectations)}`);
156
+ ${printRemainingExpectations(expectations)}`));
95
157
  }
96
158
 
97
159
  }
98
- class UnexpectedCall extends Error {
99
- constructor(property, args, expectations) {
100
- super(`Didn't expect ${jestMatcherUtils.EXPECTED_COLOR(`mock${printCall(property, args)}`)} to be called.
101
160
 
102
- ${printRemainingExpectations(expectations)}`);
161
+ const printArgsDiff = (expected, actual) => {
162
+ const diff = jestDiff.diff(expected, actual, {
163
+ omitAnnotationLines: true
164
+ });
165
+ /* istanbul ignore next this is not expected in practice */
166
+
167
+ if (!diff) {
168
+ return '';
103
169
  }
104
170
 
105
- }
106
- class NotAMock extends Error {
107
- constructor() {
108
- super(`We couldn't find the mock.
171
+ const ansilessDiffLines = stripAnsi__default["default"](diff).split('\n');
172
+ let relevantDiffLines; // Strip Array [ ... ] surroundings.
173
+
174
+ if (!expected.length) {
175
+ // - Array []
176
+ // + Array [
177
+ // ...
178
+ // ]
179
+ relevantDiffLines = ansilessDiffLines.slice(2, -1);
180
+ } else if (!actual.length) {
181
+ // - Array [
182
+ // ...
183
+ // ]
184
+ // + Array []
185
+ relevantDiffLines = ansilessDiffLines.slice(1, -2);
186
+ } else {
187
+ // Array [
188
+ // ...
189
+ // ]
190
+ relevantDiffLines = ansilessDiffLines.slice(1, -1);
191
+ } // Strip the trailing comma.
109
192
 
110
- Make sure you're passing in an actual mock.`);
111
- }
112
193
 
113
- }
114
- class UnmetExpectations extends Error {
115
- constructor(expectations) {
116
- super(`There are unmet expectations:
194
+ const lastLine = relevantDiffLines[relevantDiffLines.length - 1].slice(0, -1);
195
+ const coloredDiffLines = [...relevantDiffLines.slice(0, -1), lastLine].map(line => {
196
+ const first = line.charAt(0);
117
197
 
118
- - ${expectations.map(e => e.toJSON()).join('\n - ')}`);
119
- }
198
+ switch (first) {
199
+ case '-':
200
+ return jestMatcherUtils.EXPECTED_COLOR(line);
120
201
 
121
- }
122
- /**
123
- * Merge property accesses and method calls for the same property
124
- * into a single call.
125
- *
126
- * @example
127
- * mergeCalls({ foo: [{ arguments: undefined }, { arguments: [1, 2, 3] }] }
128
- * // returns { foo: [{ arguments: [1, 2, 3] } }
129
- */
202
+ case '+':
203
+ return jestMatcherUtils.RECEIVED_COLOR(line);
130
204
 
131
- const mergeCalls = callMap => new Map(Array.from(callMap.entries()).map(([property, calls]) => {
132
- const hasMethodCalls = calls.some(call => call.arguments);
133
- const hasPropertyAccesses = calls.some(call => !call.arguments);
205
+ default:
206
+ return line;
207
+ }
208
+ });
209
+ return coloredDiffLines.join('\n');
210
+ };
211
+ const printExpectationDiff = (e, args) => {
212
+ var _e$args;
134
213
 
135
- if (hasMethodCalls && hasPropertyAccesses) {
136
- return [property, calls.filter(call => call.arguments)];
214
+ if (!((_e$args = e.args) != null && _e$args.length)) {
215
+ return '';
137
216
  }
138
217
 
139
- return [property, calls];
140
- }));
141
-
142
- class UnexpectedCalls extends Error {
143
- constructor(unexpectedCalls, expectations) {
144
- const printedCalls = Array.from(mergeCalls(unexpectedCalls).entries()).map(([property, calls]) => calls.map(call => call.arguments ? jestMatcherUtils.EXPECTED_COLOR(`mock${printCall(property, call.arguments)}`) : jestMatcherUtils.EXPECTED_COLOR(`mock${printProperty(property)}`)).join('\n - ')).join('\n - ');
145
- super(`The following calls were unexpected:
218
+ const {
219
+ actual,
220
+ expected
221
+ } = getMatcherDiffs(e.args, args);
222
+ return printArgsDiff(expected, actual);
223
+ };
224
+ const printDiffForAllExpectations = (expectations, actual) => expectations.map(e => {
225
+ const diff = printExpectationDiff(e, actual);
146
226
 
147
- - ${printedCalls}
227
+ if (diff) {
228
+ return `${e.toString()}
229
+ ${jestMatcherUtils.EXPECTED_COLOR('- Expected')}
230
+ ${jestMatcherUtils.RECEIVED_COLOR('+ Received')}
148
231
 
149
- ${printRemainingExpectations(expectations)}`);
232
+ ${diff}`;
150
233
  }
151
234
 
152
- }
153
- class NestedWhen extends Error {
154
- constructor(parentProp, childProp) {
155
- const snippet = `
156
- const parentMock = mock<T1>();
157
- const childMock = mock<T2>();
235
+ return undefined;
236
+ }).filter(x => x).join('\n\n');
237
+ class UnexpectedCall extends Error {
238
+ constructor(property, args, expectations) {
239
+ const header = `Didn't expect ${printCall(property, args)} to be called.`;
240
+ const propertyExpectations = expectations.filter(e => e.property === property);
158
241
 
159
- when(() => childMock${printProperty(childProp)}).thenReturn(...);
160
- when(() => parentMock${printProperty(parentProp)}).thenReturn(childMock)
161
- `;
162
- super(`Setting an expectation on a nested property is not supported.
242
+ if (propertyExpectations.length) {
243
+ var _propertyExpectations;
163
244
 
164
- You can return an object directly when the first property is accessed,
165
- or you can even return a separate mock:
166
- ${snippet}`);
245
+ super(jestMatcherUtils.DIM_COLOR(`${header}
246
+
247
+ Remaining expectations:
248
+ ${printDiffForAllExpectations(propertyExpectations, args)}`)); // If we have a single expectation we can attach the actual/expected args
249
+ // to the error instance, so that an IDE may show its own diff for them.
250
+
251
+ this.matcherResult = void 0;
252
+
253
+ if (propertyExpectations.length === 1 && (_propertyExpectations = propertyExpectations[0].args) != null && _propertyExpectations.length) {
254
+ const {
255
+ actual,
256
+ expected
257
+ } = getMatcherDiffs(propertyExpectations[0].args, args);
258
+ this.matcherResult = {
259
+ actual,
260
+ expected
261
+ };
262
+ }
263
+ } else {
264
+ super(jestMatcherUtils.DIM_COLOR(`${header}
265
+
266
+ No remaining expectations.`));
267
+ this.matcherResult = void 0;
268
+ }
167
269
  }
168
270
 
169
271
  }
@@ -171,49 +273,49 @@ ${snippet}`);
171
273
  exports.UnexpectedProperty = void 0;
172
274
 
173
275
  (function (UnexpectedProperty) {
174
- /**
175
- * Throw an error immediately.
176
- *
177
- * @example
178
- * // Will throw "Didn't expect foo to be accessed".
179
- * const { foo } = service;
180
- *
181
- * // Will throw "Didn't expect foo to be accessed",
182
- * // without printing the arguments.
183
- * foo(42);
276
+ /**
277
+ * Throw an error immediately.
278
+ *
279
+ * @example
280
+ * // Will throw "Didn't expect foo to be accessed".
281
+ * const { foo } = service;
282
+ *
283
+ * // Will throw "Didn't expect foo to be accessed",
284
+ * // without printing the arguments.
285
+ * foo(42);
184
286
  */
185
287
  UnexpectedProperty[UnexpectedProperty["THROW"] = 0] = "THROW";
186
- /**
187
- * Return a function that will throw if called. This can be useful if your
188
- * code destructures a function but never calls it.
189
- *
190
- * It will also improve error messages for unexpected calls because arguments
191
- * will be captured instead of throwing immediately on the property access.
192
- *
193
- * The function will be returned even if the property is not supposed to be a
194
- * function. This could cause weird behavior at runtime, when your code expects
195
- * e.g. a number and gets a function instead.
196
- *
197
- * @example
198
- * // This will NOT throw.
199
- * const { foo } = service;
200
- *
201
- * // This will NOT throw, and might produce unexpected results.
202
- * foo > 0
203
- *
204
- * // Will throw "Didn't expect foo(42) to be called".
205
- * foo(42);
288
+ /**
289
+ * Return a function that will throw if called. This can be useful if your
290
+ * code destructures a function but never calls it.
291
+ *
292
+ * It will also improve error messages for unexpected calls because arguments
293
+ * will be captured instead of throwing immediately on the property access.
294
+ *
295
+ * The function will be returned even if the property is not supposed to be a
296
+ * function. This could cause weird behavior at runtime, when your code expects
297
+ * e.g. a number and gets a function instead.
298
+ *
299
+ * @example
300
+ * // This will NOT throw.
301
+ * const { foo } = service;
302
+ *
303
+ * // This will NOT throw, and might produce unexpected results.
304
+ * foo > 0
305
+ *
306
+ * // Will throw "Didn't expect foo(42) to be called".
307
+ * foo(42);
206
308
  */
207
309
 
208
310
  UnexpectedProperty[UnexpectedProperty["CALL_THROW"] = 1] = "CALL_THROW";
209
311
  })(exports.UnexpectedProperty || (exports.UnexpectedProperty = {}));
210
312
 
211
- /**
212
- * Unbox the expectation's return value.
213
- *
214
- * If the value is an error then throw it.
215
- *
216
- * If the value is a promise then resolve/reject it.
313
+ /**
314
+ * Unbox the expectation's return value.
315
+ *
316
+ * If the value is an error then throw it.
317
+ *
318
+ * If the value is a promise then resolve/reject it.
217
319
  */
218
320
  const unboxReturnValue = ({
219
321
  isError,
@@ -243,9 +345,9 @@ const unboxReturnValue = ({
243
345
  return value;
244
346
  };
245
347
 
246
- /**
247
- * An expectation repository with a configurable behavior for
248
- * unexpected property access.
348
+ /**
349
+ * An expectation repository with a configurable behavior for
350
+ * unexpected property access.
249
351
  */
250
352
 
251
353
  class FlexibleRepository {
@@ -417,16 +519,16 @@ class FlexibleRepository {
417
519
 
418
520
  }
419
521
 
420
- /**
421
- * Matches a call with more parameters than expected because it is assumed the
422
- * compiler will check that those parameters are optional.
423
- *
424
- * @example
425
- * new StrongExpectation(
426
- * 'bar',
427
- * deepEquals([1, 2, 3]),
428
- * 23
429
- * ).matches('bar', [1, 2, 3]) === true;
522
+ /**
523
+ * Matches a call with more parameters than expected because it is assumed the
524
+ * compiler will check that those parameters are optional.
525
+ *
526
+ * @example
527
+ * new StrongExpectation(
528
+ * 'bar',
529
+ * deepEquals([1, 2, 3]),
530
+ * 23
531
+ * ).matches('bar', [1, 2, 3]) === true;
430
532
  */
431
533
 
432
534
  class StrongExpectation {
@@ -480,13 +582,58 @@ class StrongExpectation {
480
582
  return this.args.every((arg, i) => arg.matches(received[i]));
481
583
  }
482
584
 
483
- toJSON() {
585
+ toString() {
484
586
  return printExpectation(this.property, this.args, this.returnValue, this.min, this.max);
485
587
  }
486
588
 
487
589
  }
488
590
 
489
- class PendingExpectationWithFactory {
591
+ class UnfinishedExpectation extends Error {
592
+ constructor(property, args) {
593
+ super(`There is an unfinished pending expectation:
594
+
595
+ ${printWhen(property, args)}
596
+
597
+ Please finish it by setting a return value even if the value
598
+ is undefined.`);
599
+ }
600
+
601
+ }
602
+ class MissingWhen extends Error {
603
+ constructor() {
604
+ super(`You tried setting a return value without an expectation.
605
+
606
+ Every call to set a return value must be preceded by an expectation.`);
607
+ }
608
+
609
+ }
610
+ class NotAMock extends Error {
611
+ constructor() {
612
+ super(`We couldn't find the mock.
613
+
614
+ Make sure you're passing in an actual mock.`);
615
+ }
616
+
617
+ }
618
+ class NestedWhen extends Error {
619
+ constructor(parentProp, childProp) {
620
+ const snippet = `
621
+ const parentMock = mock<T1>();
622
+ const childMock = mock<T2>();
623
+
624
+ when(() => childMock${printProperty(childProp)}).thenReturn(...);
625
+ when(() => parentMock${printProperty(parentProp)}).thenReturn(childMock)
626
+ `;
627
+ super(`Setting an expectation on a nested property is not supported.
628
+
629
+ You can return an object directly when the first property is accessed,
630
+ or you can even return a separate mock:
631
+ ${snippet}`);
632
+ }
633
+
634
+ }
635
+
636
+ class ExpectationBuilderWithFactory {
490
637
  constructor(createExpectation, concreteMatcher, exactParams) {
491
638
  this.createExpectation = void 0;
492
639
  this.concreteMatcher = void 0;
@@ -500,7 +647,7 @@ class PendingExpectationWithFactory {
500
647
 
501
648
  setProperty(value) {
502
649
  if (this.property) {
503
- throw new UnfinishedExpectation(this);
650
+ throw new UnfinishedExpectation(this.property, this.args);
504
651
  }
505
652
 
506
653
  this.property = value;
@@ -521,10 +668,6 @@ class PendingExpectationWithFactory {
521
668
  return expectation;
522
669
  }
523
670
 
524
- toJSON() {
525
- return printWhen(this.property, this.args);
526
- }
527
-
528
671
  }
529
672
 
530
673
  function _extends() {
@@ -545,33 +688,6 @@ function _extends() {
545
688
  return _extends.apply(this, arguments);
546
689
  }
547
690
 
548
- /**
549
- * Match a custom predicate.
550
- *
551
- * @param cb Will receive the value and returns whether it matches.
552
- * @param toJSON An optional function that should return a string that will be
553
- * used when the matcher needs to be printed in an error message. By default,
554
- * it stringifies `cb`.
555
- *
556
- * @example
557
- * const fn = mock<(x: number) => number>();
558
- * when(() => fn(It.matches(x => x >= 0))).returns(42);
559
- *
560
- * fn(2) === 42
561
- * fn(-1) // throws
562
- */
563
-
564
- const matches = (cb, {
565
- toJSON = () => `matches(${cb.toString()})`
566
- } = {}) => {
567
- const matcher = {
568
- [MATCHER_SYMBOL]: true,
569
- matches: arg => cb(arg),
570
- toJSON
571
- };
572
- return matcher;
573
- };
574
-
575
691
  const removeUndefined = object => {
576
692
  if (Array.isArray(object)) {
577
693
  return object.map(x => removeUndefined(x));
@@ -583,16 +699,17 @@ const removeUndefined = object => {
583
699
 
584
700
  return lodash.omitBy(object, lodash.isUndefined);
585
701
  };
586
- /**
587
- * Compare values using deep equality.
588
- *
589
- * @param expected
590
- * @param strict By default, this matcher will treat a missing key in an object
591
- * and a key with the value `undefined` as not equal. It will also consider
592
- * non `Object` instances with different constructors as not equal. Setting
593
- * this to `false` will consider the objects in both cases as equal.
594
- *
595
- * @see It.is A matcher that uses strict equality.
702
+ /**
703
+ * Compare values using deep equality.
704
+ *
705
+ * @param expected
706
+ * @param strict By default, this matcher will treat a missing key in an object
707
+ * and a key with the value `undefined` as not equal. It will also consider
708
+ * non `Object` instances with different constructors as not equal. Setting
709
+ * this to `false` will consider the objects in both cases as equal.
710
+ *
711
+ * @see {@link It.isObject} or {@link It.isArray} if you want to nest matchers.
712
+ * @see {@link It.is} if you want to use strict equality.
596
713
  */
597
714
 
598
715
 
@@ -605,244 +722,45 @@ const deepEquals = (expected, {
605
722
 
606
723
  return lodash.isEqual(removeUndefined(actual), removeUndefined(expected));
607
724
  }, {
608
- toJSON: () => printArg(expected)
609
- });
610
- /**
611
- * Compare values using `Object.is`.
612
- *
613
- * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
614
- *
615
- * @see It.deepEquals A matcher that uses deep equality.
616
- */
617
-
618
-
619
- const is = expected => matches(actual => Object.is(actual, expected), {
620
- toJSON: () => `${jestMatcherUtils.printExpected(expected)}`
621
- });
622
- /**
623
- * Match any value, including `undefined` and `null`.
624
- *
625
- * @example
626
- * const fn = mock<(x: number, y: string) => number>();
627
- * when(() => fn(It.isAny(), It.isAny())).thenReturn(1);
628
- *
629
- * fn(23, 'foobar') === 1
630
- */
631
-
632
-
633
- const isAny = () => matches(() => true, {
634
- toJSON: () => 'anything'
635
- });
636
- /**
637
- * Recursively match an object.
638
- *
639
- * Supports nested matcher.
640
- *
641
- * @param partial An optional subset of the expected objected.
642
- *
643
- * @example
644
- * const fn = mock<(foo: { x: number, y: number }) => number>();
645
- * when(() => fn(It.isObject({ x: 23 }))).returns(42);
646
- *
647
- * fn({ x: 100, y: 200 }) // throws
648
- * fn({ x: 23, y: 200 }) // returns 42
649
- *
650
- * @example
651
- * It.isObject({ foo: It.isString() })
652
- */
653
-
654
-
655
- const isObject = partial => matches(actual => lodash.isMatchWith(actual, partial || {}, (argValue, partialValue) => {
656
- if (isMatcher(partialValue)) {
657
- return partialValue.matches(argValue);
658
- } // Let lodash handle it otherwise.
659
-
660
-
661
- return undefined;
662
- }), {
663
- toJSON: () => partial ? `object(${jestMatcherUtils.printExpected(partial)})` : 'object'
664
- });
665
- /**
666
- * Match any number.
667
- *
668
- * @example
669
- * const fn = mock<(x: number) => number>();
670
- * when(() => fn(It.isNumber())).returns(42);
671
- *
672
- * fn(20.5) === 42
673
- * fn(NaN) // throws
674
- */
675
-
676
-
677
- const isNumber = () => matches(actual => typeof actual === 'number' && !Number.isNaN(actual), {
678
- toJSON: () => 'number'
679
- });
680
- /**
681
- * Match a string, potentially by a pattern.
682
- *
683
- * @param matching The string has to match this RegExp.
684
- * @param containing The string has to contain this substring.
685
- *
686
- * @example
687
- * const fn = mock<(x: string, y: string) => number>();
688
- * when(() => fn(It.isString(), It.isString({ containing: 'bar' }))).returns(42);
689
- *
690
- * fn('foo', 'baz') // throws
691
- * fn('foo', 'bar') === 42
692
- */
693
-
694
-
695
- const isString = ({
696
- matching,
697
- containing
698
- } = {}) => {
699
- if (matching && containing) {
700
- throw new Error('You can only pass `matching` or `containing`, not both.');
701
- }
702
-
703
- return matches(actual => {
704
- var _matching$test;
705
-
706
- if (typeof actual !== 'string') {
707
- return false;
708
- }
709
-
710
- if (containing) {
711
- return actual.indexOf(containing) !== -1;
712
- }
713
-
714
- return (_matching$test = matching == null ? void 0 : matching.test(actual)) != null ? _matching$test : true;
715
- }, {
716
- toJSON: () => containing || matching ? `string(${jestMatcherUtils.printExpected(containing || matching)})` : 'string'
717
- });
718
- };
719
- /**
720
- * Match an array.
721
- *
722
- * Supports nested matchers.
723
- *
724
- * @param containing If given, the matched array has to contain ALL of these
725
- * elements in ANY order.
726
- *
727
- * @example
728
- * const fn = mock<(arr: number[]) => number>();
729
- * when(() => fn(It.isArray())).thenReturn(1);
730
- * when(() => fn(It.isArray([2, 3]))).thenReturn(2);
731
- *
732
- * fn({ length: 1, 0: 42 }) // throws
733
- * fn([]) === 1
734
- * fn([3, 2, 1]) === 2
735
- *
736
- * @example
737
- * It.isArray([It.isString({ containing: 'foobar' })])
738
- */
739
-
740
-
741
- const isArray = containing => matches(actual => {
742
- if (!Array.isArray(actual)) {
743
- return false;
744
- }
745
-
746
- if (!containing) {
747
- return true;
748
- }
749
-
750
- return containing.every(x => actual.find(y => {
751
- if (isMatcher(x)) {
752
- return x.matches(y);
753
- }
754
-
755
- return deepEquals(x).matches(y);
756
- }) !== undefined);
757
- }, {
758
- toJSON: () => containing ? `array(${jestMatcherUtils.printExpected(containing)})` : 'array'
725
+ toString: () => printValue(expected),
726
+ getDiff: actual => ({
727
+ actual,
728
+ expected
729
+ })
759
730
  });
760
- /**
761
- * Matches anything and stores the received value.
762
- *
763
- * This should not be needed for most cases, but can be useful if you need
764
- * access to a complex argument outside the expectation e.g. to test a
765
- * callback.
766
- *
767
- * @param name If given, this name will be printed in error messages.
768
- *
769
- * @example
770
- * const fn = mock<(cb: (value: number) => number) => void>();
771
- * const matcher = It.willCapture();
772
- * when(() => fn(matcher)).thenReturn();
773
- *
774
- * fn(x => x + 1);
775
- * matcher.value?.(3) === 4
776
- */
777
-
778
-
779
- const willCapture = name => {
780
- let capturedValue;
781
- const matcher = {
782
- [MATCHER_SYMBOL]: true,
783
- matches: actual => {
784
- capturedValue = actual;
785
- return true;
786
- },
787
- toJSON: () => name != null ? name : 'captures',
788
-
789
- get value() {
790
- return capturedValue;
791
- }
792
-
793
- };
794
- return matcher;
795
- };
796
- /**
797
- * Contains argument matchers that can be used to ignore arguments in an
798
- * expectation or to match complex arguments.
799
- */
800
-
801
-
802
- const It = {
803
- matches,
804
- deepEquals,
805
- is,
806
- isAny,
807
- isObject,
808
- isNumber,
809
- isString,
810
- isArray,
811
- willCapture
812
- };
813
731
 
814
732
  const defaults = {
815
- concreteMatcher: It.deepEquals,
733
+ concreteMatcher: deepEquals,
816
734
  unexpectedProperty: exports.UnexpectedProperty.CALL_THROW,
817
735
  exactParams: false
818
736
  };
819
737
  let currentDefaults = defaults;
820
- /**
821
- * Override strong-mock's defaults.
822
- *
823
- * @param newDefaults These will be applied to the library defaults. Multiple
824
- * calls don't stack e.g. calling this with `{}` will clear any previously
825
- * applied defaults.
738
+ /**
739
+ * Override strong-mock's defaults.
740
+ *
741
+ * @param newDefaults These will be applied to the library defaults. Multiple
742
+ * calls don't stack e.g. calling this with `{}` will clear any previously
743
+ * applied defaults.
826
744
  */
827
745
 
828
746
  const setDefaults = newDefaults => {
829
747
  currentDefaults = _extends({}, defaults, newDefaults);
830
748
  };
831
749
 
832
- /**
833
- * Since `when` doesn't receive the mock subject (because we can't make it
834
- * consistently return it from `mock()`, `mock.foo` and `mock.bar()`) we need
835
- * to store a global state for the currently active mock.
836
- *
837
- * We also want to throw in the following case:
838
- *
839
- * ```
840
- * when(() => mock()) // forgot returns here
841
- * when(() => mock()) // should throw
842
- * ```
843
- *
844
- * For that reason we can't just store the currently active mock, but also
845
- * whether we finished the expectation or not.
750
+ /**
751
+ * Since `when` doesn't receive the mock subject (because we can't make it
752
+ * consistently return it from `mock()`, `mock.foo` and `mock.bar()`) we need
753
+ * to store a global state for the currently active mock.
754
+ *
755
+ * We also want to throw in the following case:
756
+ *
757
+ * ```
758
+ * when(() => mock()) // forgot returns here
759
+ * when(() => mock()) // should throw
760
+ * ```
761
+ *
762
+ * For that reason we can't just store the currently active mock, but also
763
+ * whether we finished the expectation or not.
846
764
  */
847
765
 
848
766
  let activeMock;
@@ -850,11 +768,11 @@ const setActiveMock = mock => {
850
768
  activeMock = mock;
851
769
  };
852
770
  const getActiveMock = () => activeMock;
853
- /**
854
- * Store a global map of all mocks created and their state.
855
- *
856
- * This is needed because we can't reliably pass the state between `when`
857
- * and `thenReturn`.
771
+ /**
772
+ * Store a global map of all mocks created and their state.
773
+ *
774
+ * This is needed because we can't reliably pass the state between `when`
775
+ * and `thenReturn`.
858
776
  */
859
777
 
860
778
  const mockMap = new Map();
@@ -870,7 +788,9 @@ const setMockState = (mock, state) => {
870
788
  };
871
789
  const getAllMocks = () => Array.from(mockMap.entries());
872
790
 
873
- const createProxy = traps => // eslint-disable-next-line no-empty-function
791
+ const createProxy = traps => // The Proxy target MUST be a function, otherwise we can't use the `apply` trap:
792
+ // https://262.ecma-international.org/6.0/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist
793
+ // eslint-disable-next-line no-empty-function,@typescript-eslint/no-empty-function
874
794
  new Proxy(
875
795
  /* istanbul ignore next */
876
796
  () => {}, {
@@ -907,7 +827,7 @@ new Proxy(
907
827
 
908
828
  });
909
829
 
910
- const createStub = (repo, pendingExpectation, getCurrentMode) => {
830
+ const createStub = (repo, builder, getCurrentMode) => {
911
831
  const stub = createProxy({
912
832
  property: property => {
913
833
  if (getCurrentMode() === Mode.CALL) {
@@ -915,13 +835,13 @@ const createStub = (repo, pendingExpectation, getCurrentMode) => {
915
835
  }
916
836
 
917
837
  setActiveMock(stub);
918
- pendingExpectation.setProperty(property);
838
+ builder.setProperty(property);
919
839
  return createProxy({
920
840
  property: childProp => {
921
841
  throw new NestedWhen(property, childProp);
922
842
  },
923
843
  apply: args => {
924
- pendingExpectation.setArgs(args);
844
+ builder.setArgs(args);
925
845
  },
926
846
  ownKeys: () => {
927
847
  throw new Error('Spreading during an expectation is not supported.');
@@ -934,8 +854,8 @@ const createStub = (repo, pendingExpectation, getCurrentMode) => {
934
854
  }
935
855
 
936
856
  setActiveMock(stub);
937
- pendingExpectation.setProperty(ApplyProp);
938
- pendingExpectation.setArgs(args);
857
+ builder.setProperty(ApplyProp);
858
+ builder.setArgs(args);
939
859
  return undefined;
940
860
  },
941
861
  ownKeys: () => {
@@ -965,26 +885,26 @@ const setMode = mode => {
965
885
  };
966
886
 
967
887
  const getMode = () => currentMode;
968
- /**
969
- * Create a type safe mock.
970
- *
971
- * @see {@link when} Set expectations on the mock using `when`.
972
- *
973
- * @param options Configure the options for this specific mock, overriding any
974
- * defaults that were set with {@link setDefaults}.
975
- * @param options.unexpectedProperty Controls what happens when an unexpected
976
- * property is accessed.
977
- * @param options.concreteMatcher The matcher that will be used when one isn't
978
- * specified explicitly.
979
- * @param options.exactParams Controls whether the number of received arguments
980
- * has to match the expectation.
981
- *
982
- * @example
983
- * const fn = mock<() => number>();
984
- *
985
- * when(() => fn()).thenReturn(23);
986
- *
987
- * fn() === 23;
888
+ /**
889
+ * Create a type safe mock.
890
+ *
891
+ * @see {@link when} Set expectations on the mock using `when`.
892
+ *
893
+ * @param options Configure the options for this specific mock, overriding any
894
+ * defaults that were set with {@link setDefaults}.
895
+ * @param options.unexpectedProperty Controls what happens when an unexpected
896
+ * property is accessed.
897
+ * @param options.concreteMatcher The matcher that will be used when one isn't
898
+ * specified explicitly.
899
+ * @param options.exactParams Controls whether the number of received arguments
900
+ * has to match the expectation.
901
+ *
902
+ * @example
903
+ * const fn = mock<() => number>();
904
+ *
905
+ * when(() => fn()).thenReturn(23);
906
+ *
907
+ * fn() === 23;
988
908
  */
989
909
 
990
910
 
@@ -999,11 +919,11 @@ const mock = ({
999
919
  exactParams: exactParams != null ? exactParams : currentDefaults.exactParams
1000
920
  };
1001
921
  const repository = new FlexibleRepository(options.unexpectedProperty);
1002
- const pendingExpectation = new PendingExpectationWithFactory(strongExpectationFactory, options.concreteMatcher, options.exactParams);
1003
- const stub = createStub(repository, pendingExpectation, getMode);
922
+ const builder = new ExpectationBuilderWithFactory(strongExpectationFactory, options.concreteMatcher, options.exactParams);
923
+ const stub = createStub(repository, builder, getMode);
1004
924
  setMockState(stub, {
1005
925
  repository,
1006
- pendingExpectation,
926
+ builder,
1007
927
  options
1008
928
  });
1009
929
  return stub;
@@ -1048,8 +968,8 @@ const createInvocationCount = expectation => ({
1048
968
 
1049
969
  });
1050
970
 
1051
- const finishPendingExpectation = (returnValue, pendingExpectation, repo) => {
1052
- const finishedExpectation = pendingExpectation.finish(returnValue);
971
+ const finishExpectation = (returnValue, builder, repo) => {
972
+ const finishedExpectation = builder.finish(returnValue);
1053
973
  repo.add(finishedExpectation);
1054
974
  return createInvocationCount(finishedExpectation);
1055
975
  };
@@ -1066,54 +986,54 @@ const getError = errorOrMessage => {
1066
986
  return new Error();
1067
987
  };
1068
988
 
1069
- const createReturns = (pendingExpectation, repository) => ({
1070
- thenReturn: returnValue => finishPendingExpectation( // This will handle both thenReturn(23) and thenReturn(Promise.resolve(3)).
989
+ const createReturns = (builder, repository) => ({
990
+ thenReturn: returnValue => finishExpectation( // This will handle both thenReturn(23) and thenReturn(Promise.resolve(3)).
1071
991
  {
1072
992
  value: returnValue,
1073
993
  isError: false,
1074
994
  isPromise: false
1075
- }, pendingExpectation, repository),
1076
- thenThrow: errorOrMessage => finishPendingExpectation({
995
+ }, builder, repository),
996
+ thenThrow: errorOrMessage => finishExpectation({
1077
997
  value: getError(errorOrMessage),
1078
998
  isError: true,
1079
999
  isPromise: false
1080
- }, pendingExpectation, repository),
1081
- thenResolve: promiseValue => finishPendingExpectation({
1000
+ }, builder, repository),
1001
+ thenResolve: promiseValue => finishExpectation({
1082
1002
  value: promiseValue,
1083
1003
  isError: false,
1084
1004
  isPromise: true
1085
- }, pendingExpectation, repository),
1086
- thenReject: errorOrMessage => finishPendingExpectation({
1005
+ }, builder, repository),
1006
+ thenReject: errorOrMessage => finishExpectation({
1087
1007
  value: getError(errorOrMessage),
1088
1008
  isError: true,
1089
1009
  isPromise: true
1090
- }, pendingExpectation, repository)
1010
+ }, builder, repository)
1091
1011
  });
1092
1012
 
1093
- /**
1094
- * Set an expectation on a mock.
1095
- *
1096
- * The expectation must be finished by setting a return value, even if the value
1097
- * is `undefined`.
1098
- *
1099
- * If a call happens that was not expected then the mock will throw an error.
1100
- * By default, the call is expected to only be made once. Use the invocation
1101
- * count helpers to expect a call multiple times.
1102
- *
1103
- * @param expectation A callback to set the expectation on your mock. The
1104
- * callback must return the value from the mock to properly infer types.
1105
- *
1106
- * @example
1107
- * const fn = mock<() => void>();
1108
- * when(() => fn()).thenReturn(undefined);
1109
- *
1110
- * @example
1111
- * const fn = mock<() => number>();
1112
- * when(() => fn()).thenReturn(42).atMost(3);
1113
- *
1114
- * @example
1115
- * const fn = mock<(x: number) => Promise<number>();
1116
- * when(() => fn(23)).thenResolve(42);
1013
+ /**
1014
+ * Set an expectation on a mock.
1015
+ *
1016
+ * The expectation must be finished by setting a return value, even if the value
1017
+ * is `undefined`.
1018
+ *
1019
+ * If a call happens that was not expected then the mock will throw an error.
1020
+ * By default, the call is expected to only be made once. Use the invocation
1021
+ * count helpers to expect a call multiple times.
1022
+ *
1023
+ * @param expectation A callback to set the expectation on your mock. The
1024
+ * callback must return the value from the mock to properly infer types.
1025
+ *
1026
+ * @example
1027
+ * const fn = mock<() => void>();
1028
+ * when(() => fn()).thenReturn(undefined);
1029
+ *
1030
+ * @example
1031
+ * const fn = mock<() => number>();
1032
+ * when(() => fn()).thenReturn(42).atMost(3);
1033
+ *
1034
+ * @example
1035
+ * const fn = mock<(x: number) => Promise<number>();
1036
+ * when(() => fn(23)).thenResolve(42);
1117
1037
  */
1118
1038
 
1119
1039
  const when = expectation => {
@@ -1121,32 +1041,32 @@ const when = expectation => {
1121
1041
  expectation();
1122
1042
  setMode(Mode.CALL);
1123
1043
  const {
1124
- pendingExpectation,
1044
+ builder,
1125
1045
  repository
1126
1046
  } = getMockState(getActiveMock());
1127
- return createReturns(pendingExpectation, repository);
1047
+ return createReturns(builder, repository);
1128
1048
  };
1129
1049
 
1130
- /**
1131
- * Remove any remaining expectations on the given mock.
1132
- *
1133
- * @example
1134
- * const fn = mock<() => number>();
1135
- *
1136
- * when(() => fn()).thenReturn(23);
1137
- *
1138
- * reset(fn);
1139
- *
1140
- * fn(); // throws
1050
+ /**
1051
+ * Remove any remaining expectations on the given mock.
1052
+ *
1053
+ * @example
1054
+ * const fn = mock<() => number>();
1055
+ *
1056
+ * when(() => fn()).thenReturn(23);
1057
+ *
1058
+ * reset(fn);
1059
+ *
1060
+ * fn(); // throws
1141
1061
  */
1142
1062
 
1143
1063
  const reset = mock => {
1144
1064
  getMockState(mock).repository.clear();
1145
1065
  };
1146
- /**
1147
- * Reset all existing mocks.
1148
- *
1149
- * @see reset
1066
+ /**
1067
+ * Reset all existing mocks.
1068
+ *
1069
+ * @see reset
1150
1070
  */
1151
1071
 
1152
1072
  const resetAll = () => {
@@ -1155,6 +1075,46 @@ const resetAll = () => {
1155
1075
  });
1156
1076
  };
1157
1077
 
1078
+ class UnmetExpectations extends Error {
1079
+ constructor(expectations) {
1080
+ super(jestMatcherUtils.DIM_COLOR(`There are unmet expectations:
1081
+
1082
+ - ${expectations.map(e => e.toString()).join('\n - ')}`));
1083
+ }
1084
+
1085
+ }
1086
+ /**
1087
+ * Merge property accesses and method calls for the same property
1088
+ * into a single call.
1089
+ *
1090
+ * @example
1091
+ * mergeCalls({ getData: [{ arguments: undefined }, { arguments: [1, 2, 3] }] }
1092
+ * // returns { getData: [{ arguments: [1, 2, 3] } }
1093
+ */
1094
+
1095
+ const mergeCalls = callMap => new Map(Array.from(callMap.entries()).map(([property, calls]) => {
1096
+ const hasMethodCalls = calls.some(call => call.arguments);
1097
+ const hasPropertyAccesses = calls.some(call => !call.arguments);
1098
+
1099
+ if (hasMethodCalls && hasPropertyAccesses) {
1100
+ return [property, calls.filter(call => call.arguments)];
1101
+ }
1102
+
1103
+ return [property, calls];
1104
+ }));
1105
+
1106
+ class UnexpectedCalls extends Error {
1107
+ constructor(unexpectedCalls, expectations) {
1108
+ const printedCalls = Array.from(mergeCalls(unexpectedCalls).entries()).map(([property, calls]) => calls.map(call => printCall(property, call.arguments)).join('\n - ')).join('\n - ');
1109
+ super(jestMatcherUtils.DIM_COLOR(`The following calls were unexpected:
1110
+
1111
+ - ${printedCalls}
1112
+
1113
+ ${printRemainingExpectations(expectations)}`));
1114
+ }
1115
+
1116
+ }
1117
+
1158
1118
  const verifyRepo = repository => {
1159
1119
  const unmetExpectations = repository.getUnmet();
1160
1120
 
@@ -1168,22 +1128,22 @@ const verifyRepo = repository => {
1168
1128
  throw new UnexpectedCalls(callStats.unexpected, unmetExpectations);
1169
1129
  }
1170
1130
  };
1171
- /**
1172
- * Verify that all expectations on the given mock have been met.
1173
- *
1174
- * @throws Will throw if there are remaining expectations that were set
1175
- * using `when` and that weren't met.
1176
- *
1177
- * @throws Will throw if any unexpected calls happened. Normally those
1178
- * calls throw on their own, but the error might be caught by the code
1179
- * being tested.
1180
- *
1181
- * @example
1182
- * const fn = mock<() => number>();
1183
- *
1184
- * when(() => fn()).thenReturn(23);
1185
- *
1186
- * verify(fn); // throws
1131
+ /**
1132
+ * Verify that all expectations on the given mock have been met.
1133
+ *
1134
+ * @throws Will throw if there are remaining expectations that were set
1135
+ * using `when` and that weren't met.
1136
+ *
1137
+ * @throws Will throw if any unexpected calls happened. Normally those
1138
+ * calls throw on their own, but the error might be caught by the code
1139
+ * being tested.
1140
+ *
1141
+ * @example
1142
+ * const fn = mock<() => number>();
1143
+ *
1144
+ * when(() => fn()).thenReturn(23);
1145
+ *
1146
+ * verify(fn); // throws
1187
1147
  */
1188
1148
 
1189
1149
  const verify = mock => {
@@ -1192,10 +1152,10 @@ const verify = mock => {
1192
1152
  } = getMockState(mock);
1193
1153
  verifyRepo(repository);
1194
1154
  };
1195
- /**
1196
- * Verify all existing mocks.
1197
- *
1198
- * @see verify
1155
+ /**
1156
+ * Verify all existing mocks.
1157
+ *
1158
+ * @see verify
1199
1159
  */
1200
1160
 
1201
1161
  const verifyAll = () => {
@@ -1204,7 +1164,345 @@ const verifyAll = () => {
1204
1164
  });
1205
1165
  };
1206
1166
 
1207
- exports.It = It;
1167
+ /**
1168
+ * Compare values using `Object.is`.
1169
+ *
1170
+ * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
1171
+ *
1172
+ * @see It.deepEquals A matcher that uses deep equality.
1173
+ */
1174
+
1175
+ const is = expected => matches(actual => Object.is(actual, expected), {
1176
+ toString: () => `${printValue(expected)}`,
1177
+ getDiff: actual => ({
1178
+ actual,
1179
+ expected
1180
+ })
1181
+ });
1182
+
1183
+ /**
1184
+ * Match any value, including `undefined` and `null`.
1185
+ *
1186
+ * @example
1187
+ * const fn = mock<(x: number, y: string) => number>();
1188
+ * when(() => fn(It.isAny(), It.isAny())).thenReturn(1);
1189
+ *
1190
+ * fn(23, 'foobar') === 1
1191
+ */
1192
+
1193
+ const isAny = () => matches(() => true, {
1194
+ toString: () => 'Matcher<any>'
1195
+ });
1196
+
1197
+ /**
1198
+ * Match an array.
1199
+ *
1200
+ * Supports nested matchers.
1201
+ *
1202
+ * @param containing If given, the matched array has to contain ALL of these
1203
+ * elements in ANY order.
1204
+ *
1205
+ * @example
1206
+ * const fn = mock<(arr: number[]) => number>();
1207
+ * when(() => fn(It.isArray())).thenReturn(1);
1208
+ * when(() => fn(It.isArray([2, 3]))).thenReturn(2);
1209
+ *
1210
+ * fn({ length: 1, 0: 42 }) // throws
1211
+ * fn([]) === 1
1212
+ * fn([3, 2, 1]) === 2
1213
+ *
1214
+ * @example
1215
+ * It.isArray([It.isString({ containing: 'foobar' })])
1216
+ */
1217
+
1218
+ const isArray = containing => matches(actual => {
1219
+ if (!Array.isArray(actual)) {
1220
+ return false;
1221
+ }
1222
+
1223
+ if (!containing) {
1224
+ return true;
1225
+ }
1226
+
1227
+ return containing.every(x => actual.find(y => {
1228
+ if (isMatcher(x)) {
1229
+ return x.matches(y);
1230
+ }
1231
+
1232
+ return deepEquals(x).matches(y);
1233
+ }) !== undefined);
1234
+ }, {
1235
+ toString: () => containing ? `array(${printValue(containing)})` : 'array',
1236
+ getDiff: actual => {
1237
+ if (containing) {
1238
+ return {
1239
+ actual,
1240
+ expected: `Matcher<array>([${containing.map(value => {
1241
+ if (isMatcher(value)) {
1242
+ return value.toString();
1243
+ }
1244
+
1245
+ return value;
1246
+ }).join(', ')}])`
1247
+ };
1248
+ }
1249
+
1250
+ return {
1251
+ actual: `${printValue(actual)} (${typeof actual})`,
1252
+ expected: 'Matcher<array>'
1253
+ };
1254
+ }
1255
+ });
1256
+
1257
+ /**
1258
+ * Match any number.
1259
+ *
1260
+ * @example
1261
+ * const fn = mock<(x: number) => number>();
1262
+ * when(() => fn(It.isNumber())).returns(42);
1263
+ *
1264
+ * fn(20.5) === 42
1265
+ * fn(NaN) // throws
1266
+ */
1267
+
1268
+ const isNumber = () => matches(actual => typeof actual === 'number' && !Number.isNaN(actual), {
1269
+ toString: () => 'Matcher<number>',
1270
+ getDiff: actual => ({
1271
+ actual: `${printValue(actual)} (${typeof actual})`,
1272
+ expected: 'Matcher<number>'
1273
+ })
1274
+ });
1275
+
1276
+ const looksLikeObject = value => lodash.isPlainObject(value);
1277
+
1278
+ const getExpectedObjectDiff = (actual, expected) => Object.fromEntries(Reflect.ownKeys(expected).map(key => {
1279
+ const right = expected[key]; // @ts-expect-error because we're fine with a runtime undefined
1280
+
1281
+ const left = actual == null ? void 0 : actual[key];
1282
+
1283
+ if (isMatcher(right)) {
1284
+ return [key, right.getDiff(left).expected];
1285
+ }
1286
+
1287
+ if (looksLikeObject(right)) {
1288
+ return [key, getExpectedObjectDiff(left, right)];
1289
+ }
1290
+
1291
+ return [key, right];
1292
+ }));
1293
+
1294
+ const getActualObjectDiff = (actual, expected) => {
1295
+ if (!looksLikeObject(actual)) {
1296
+ return actual;
1297
+ }
1298
+
1299
+ return Object.fromEntries(Reflect.ownKeys(expected).map(key => {
1300
+ const right = expected[key];
1301
+ const left = actual[key];
1302
+
1303
+ if (!left) {
1304
+ return [];
1305
+ }
1306
+
1307
+ if (isMatcher(right)) {
1308
+ return [key, right.getDiff(left).actual];
1309
+ }
1310
+
1311
+ if (looksLikeObject(right)) {
1312
+ return [key, getActualObjectDiff(left, right)];
1313
+ }
1314
+
1315
+ return [key, left];
1316
+ }));
1317
+ };
1318
+
1319
+ const isMatch = (actual, expected) => Reflect.ownKeys(expected).every(key => {
1320
+ const right = expected[key];
1321
+ const left = looksLikeObject(actual) ? actual[key] : undefined;
1322
+
1323
+ if (!left) {
1324
+ return false;
1325
+ }
1326
+
1327
+ if (isMatcher(right)) {
1328
+ return right.matches(left);
1329
+ }
1330
+
1331
+ if (looksLikeObject(right)) {
1332
+ return isMatch(left, right);
1333
+ }
1334
+
1335
+ return deepEquals(right).matches(left);
1336
+ });
1337
+
1338
+ const deepPrintObject = value => lodash.cloneDeepWith(value, value => {
1339
+ if (isMatcher(value)) {
1340
+ return value.toString();
1341
+ }
1342
+
1343
+ return undefined;
1344
+ });
1345
+ /**
1346
+ * Match any plain object.
1347
+ *
1348
+ * Object like values, e.g. classes and arrays, will not be matched.
1349
+ *
1350
+ * @param partial An optional subset of the expected object that will be
1351
+ * recursively matched. Supports nested matchers. Values will be
1352
+ * compared with {@link deepEquals}.
1353
+ *
1354
+ * @example
1355
+ * const fn = mock<(pos: { x: number, y: number }) => number>();
1356
+ * when(() => fn(It.isObject({ x: 23 }))).returns(42);
1357
+ *
1358
+ * fn({ x: 100, y: 200 }) // throws
1359
+ * fn({ x: 23, y: 200 }) // returns 42
1360
+ *
1361
+ * @example
1362
+ * It.isObject({ foo: It.isString() })
1363
+ */
1364
+
1365
+
1366
+ const isObject = partial => matches(actual => {
1367
+ if (!looksLikeObject(actual)) {
1368
+ return false;
1369
+ }
1370
+
1371
+ if (!partial) {
1372
+ return true;
1373
+ }
1374
+
1375
+ return isMatch(actual, partial);
1376
+ }, {
1377
+ toString: () => {
1378
+ if (!partial) {
1379
+ return 'Matcher<object>';
1380
+ }
1381
+
1382
+ return `Matcher<object>(${printValue(deepPrintObject(partial))})`;
1383
+ },
1384
+ getDiff: actual => {
1385
+ if (!partial) {
1386
+ return {
1387
+ expected: 'Matcher<object>',
1388
+ actual: `${printValue(actual)}`
1389
+ };
1390
+ }
1391
+
1392
+ return {
1393
+ actual: getActualObjectDiff(actual, partial),
1394
+ expected: getExpectedObjectDiff(actual, partial)
1395
+ };
1396
+ }
1397
+ });
1398
+
1399
+ /**
1400
+ * Match any string.
1401
+ *
1402
+ * @param matching An optional string or RegExp to match the string against.
1403
+ * If it's a string, a case-sensitive search will be performed.
1404
+ *
1405
+ * @example
1406
+ * const fn = mock<(x: string, y: string) => number>();
1407
+ * when(() => fn(It.isString(), It.isString('bar'))).returns(42);
1408
+ *
1409
+ * fn('foo', 'baz') // throws
1410
+ * fn('foo', 'bar') === 42
1411
+ */
1412
+
1413
+ const isString = matching => matches(actual => {
1414
+ if (typeof actual !== 'string') {
1415
+ return false;
1416
+ }
1417
+
1418
+ if (!matching) {
1419
+ return true;
1420
+ }
1421
+
1422
+ if (typeof matching === 'string') {
1423
+ return actual.indexOf(matching) !== -1;
1424
+ }
1425
+
1426
+ return matching.test(actual);
1427
+ }, {
1428
+ toString: () => {
1429
+ if (matching) {
1430
+ return `Matcher<string>(${matching})`;
1431
+ }
1432
+
1433
+ return 'Matcher<string>';
1434
+ },
1435
+ getDiff: actual => {
1436
+ if (matching) {
1437
+ return {
1438
+ expected: `Matcher<string>(${matching})`,
1439
+ actual
1440
+ };
1441
+ }
1442
+
1443
+ return {
1444
+ expected: 'Matcher<string>',
1445
+ actual: `${actual} (${typeof actual})`
1446
+ };
1447
+ }
1448
+ });
1449
+
1450
+ /**
1451
+ * Matches anything and stores the received value.
1452
+ *
1453
+ * This should not be needed for most cases, but can be useful if you need
1454
+ * access to a complex argument outside the expectation e.g. to test a
1455
+ * callback.
1456
+ *
1457
+ * @param name If given, this name will be printed in error messages.
1458
+ *
1459
+ * @example
1460
+ * const fn = mock<(cb: (value: number) => number) => void>();
1461
+ * const matcher = It.willCapture();
1462
+ * when(() => fn(matcher)).thenReturn();
1463
+ *
1464
+ * fn(x => x + 1);
1465
+ * matcher.value?.(3) === 4
1466
+ */
1467
+
1468
+ const willCapture = name => {
1469
+ let capturedValue;
1470
+ const matcher = {
1471
+ [MATCHER_SYMBOL]: true,
1472
+ matches: actual => {
1473
+ capturedValue = actual;
1474
+ return true;
1475
+ },
1476
+ toString: () => name ? `Capture(${name})` : 'Capture',
1477
+ getDiff: actual => ({
1478
+ actual,
1479
+ expected: actual
1480
+ }),
1481
+
1482
+ get value() {
1483
+ return capturedValue;
1484
+ }
1485
+
1486
+ };
1487
+ return matcher;
1488
+ };
1489
+
1490
+ /* istanbul ignore file */
1491
+
1492
+ var it = {
1493
+ __proto__: null,
1494
+ deepEquals: deepEquals,
1495
+ is: is,
1496
+ isAny: isAny,
1497
+ isArray: isArray,
1498
+ isNumber: isNumber,
1499
+ isObject: isObject,
1500
+ isString: isString,
1501
+ matches: matches,
1502
+ willCapture: willCapture
1503
+ };
1504
+
1505
+ exports.It = it;
1208
1506
  exports.mock = mock;
1209
1507
  exports.reset = reset;
1210
1508
  exports.resetAll = resetAll;