@vitest/expect 3.2.0-beta.1 → 3.2.0-beta.3

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.
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object");
10
10
  const GLOBAL_EXPECT = Symbol.for("expect-global");
11
11
  const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for("asymmetric-matchers-object");
12
12
 
13
+ // selectively ported from https://github.com/jest-community/jest-extended
13
14
  const customMatchers = {
14
15
  toSatisfy(actual, expected, message) {
15
16
  const { printReceived, printExpected, matcherHint } = this.utils;
@@ -81,8 +82,11 @@ function matcherHint(matcherName, received = "received", expected = "expected",
81
82
  dimString = "";
82
83
  }
83
84
  if (matcherName.includes(".")) {
85
+ // Old format: for backward compatibility,
86
+ // especially without promise or isNot options
84
87
  dimString += matcherName;
85
88
  } else {
89
+ // New format: omit period from matcherName arg
86
90
  hint += DIM_COLOR(`${dimString}.`) + matcherName;
87
91
  dimString = "";
88
92
  }
@@ -104,6 +108,8 @@ function matcherHint(matcherName, received = "received", expected = "expected",
104
108
  return hint;
105
109
  }
106
110
  const SPACE_SYMBOL = "·";
111
+ // Instead of inverse highlight which now implies a change,
112
+ // replace common spaces with middle dot at the end of any line.
107
113
  function replaceTrailingSpaces(text) {
108
114
  return text.replace(/\s+$/gm, (spaces) => SPACE_SYMBOL.repeat(spaces.length));
109
115
  }
@@ -144,6 +150,7 @@ function getCustomEqualityTesters() {
144
150
  return globalThis[JEST_MATCHERS_OBJECT].customEqualityTesters;
145
151
  }
146
152
 
153
+ // Extracted out of jasmine 2.5.2
147
154
  function equals(a, b, customTesters, strictCheck) {
148
155
  customTesters = customTesters || [];
149
156
  return eq(a, b, [], [], customTesters, strictCheck ? hasKey : hasDefinedKey);
@@ -184,6 +191,8 @@ function asymmetricMatch(a, b) {
184
191
  return b.asymmetricMatch(a);
185
192
  }
186
193
  }
194
+ // Equality function lovingly adapted from isEqual in
195
+ // [Underscore](http://underscorejs.org)
187
196
  function eq(a, b, aStack, bStack, customTesters, hasKey) {
188
197
  let result = true;
189
198
  const asymmetricResult = asymmetricMatch(a, b);
@@ -203,6 +212,7 @@ function eq(a, b, aStack, bStack, customTesters, hasKey) {
203
212
  if (Object.is(a, b)) {
204
213
  return true;
205
214
  }
215
+ // A strict comparison is necessary because `null == undefined`.
206
216
  if (a === null || b === null) {
207
217
  return a === b;
208
218
  }
@@ -214,35 +224,58 @@ function eq(a, b, aStack, bStack, customTesters, hasKey) {
214
224
  case "[object Boolean]":
215
225
  case "[object String]":
216
226
  case "[object Number]": if (typeof a !== typeof b) {
227
+ // One is a primitive, one a `new Primitive()`
217
228
  return false;
218
229
  } else if (typeof a !== "object" && typeof b !== "object") {
230
+ // both are proper primitives
219
231
  return Object.is(a, b);
220
232
  } else {
233
+ // both are `new Primitive()`s
221
234
  return Object.is(a.valueOf(), b.valueOf());
222
235
  }
223
236
  case "[object Date]": {
224
237
  const numA = +a;
225
238
  const numB = +b;
239
+ // Coerce dates to numeric primitive values. Dates are compared by their
240
+ // millisecond representations. Note that invalid dates with millisecond representations
241
+ // of `NaN` are equivalent.
226
242
  return numA === numB || Number.isNaN(numA) && Number.isNaN(numB);
227
243
  }
228
244
  case "[object RegExp]": return a.source === b.source && a.flags === b.flags;
245
+ case "[object Temporal.Instant]":
246
+ case "[object Temporal.ZonedDateTime]":
247
+ case "[object Temporal.PlainDateTime]":
248
+ case "[object Temporal.PlainDate]":
249
+ case "[object Temporal.PlainTime]":
250
+ case "[object Temporal.PlainYearMonth]":
251
+ case "[object Temporal.PlainMonthDay]": return a.equals(b);
252
+ case "[object Temporal.Duration]": return a.toString() === b.toString();
229
253
  }
230
254
  if (typeof a !== "object" || typeof b !== "object") {
231
255
  return false;
232
256
  }
257
+ // Use DOM3 method isEqualNode (IE>=9)
233
258
  if (isDomNode(a) && isDomNode(b)) {
234
259
  return a.isEqualNode(b);
235
260
  }
261
+ // Used to detect circular references.
236
262
  let length = aStack.length;
237
263
  while (length--) {
264
+ // Linear search. Performance is inversely proportional to the number of
265
+ // unique nested structures.
266
+ // circular references at same depth are equal
267
+ // circular reference is not equal to non-circular one
238
268
  if (aStack[length] === a) {
239
269
  return bStack[length] === b;
240
270
  } else if (bStack[length] === b) {
241
271
  return false;
242
272
  }
243
273
  }
274
+ // Add the first object to the stack of traversed objects.
244
275
  aStack.push(a);
245
276
  bStack.push(b);
277
+ // Recursively compare objects and arrays.
278
+ // Compare array lengths to determine if a deep comparison is necessary.
246
279
  if (className === "[object Array]" && a.length !== b.length) {
247
280
  return false;
248
281
  }
@@ -254,31 +287,42 @@ function eq(a, b, aStack, bStack, customTesters, hasKey) {
254
287
  bStack.pop();
255
288
  }
256
289
  }
290
+ // Deep compare objects.
257
291
  const aKeys = keys(a, hasKey);
258
292
  let key;
259
293
  let size = aKeys.length;
294
+ // Ensure that both objects contain the same number of properties before comparing deep equality.
260
295
  if (keys(b, hasKey).length !== size) {
261
296
  return false;
262
297
  }
263
298
  while (size--) {
264
299
  key = aKeys[size];
300
+ // Deep compare each member
265
301
  result = hasKey(b, key) && eq(a[key], b[key], aStack, bStack, customTesters, hasKey);
266
302
  if (!result) {
267
303
  return false;
268
304
  }
269
305
  }
306
+ // Remove the first object from the stack of traversed objects.
270
307
  aStack.pop();
271
308
  bStack.pop();
272
309
  return result;
273
310
  }
274
311
  function isErrorEqual(a, b, aStack, bStack, customTesters, hasKey) {
312
+ // https://nodejs.org/docs/latest-v22.x/api/assert.html#comparison-details
313
+ // - [[Prototype]] of objects are compared using the === operator.
314
+ // - Only enumerable "own" properties are considered.
315
+ // - Error names, messages, causes, and errors are always compared, even if these are not enumerable properties. errors is also compared.
275
316
  let result = Object.getPrototypeOf(a) === Object.getPrototypeOf(b) && a.name === b.name && a.message === b.message;
317
+ // check Error.cause asymmetrically
276
318
  if (typeof b.cause !== "undefined") {
277
319
  result && (result = eq(a.cause, b.cause, aStack, bStack, customTesters, hasKey));
278
320
  }
321
+ // AggregateError.errors
279
322
  if (a instanceof AggregateError && b instanceof AggregateError) {
280
323
  result && (result = eq(a.errors, b.errors, aStack, bStack, customTesters, hasKey));
281
324
  }
325
+ // spread to compare enumerable properties
282
326
  result && (result = eq({ ...a }, { ...b }, aStack, bStack, customTesters, hasKey));
283
327
  return result;
284
328
  }
@@ -328,6 +372,7 @@ function hasProperty(obj, property) {
328
372
  }
329
373
  return hasProperty(getPrototype(obj), property);
330
374
  }
375
+ // SENTINEL constants are from https://github.com/facebook/immutable-js
331
376
  const IS_KEYED_SENTINEL = "@@__IMMUTABLE_KEYED__@@";
332
377
  const IS_SET_SENTINEL = "@@__IMMUTABLE_SET__@@";
333
378
  const IS_LIST_SENTINEL = "@@__IMMUTABLE_LIST__@@";
@@ -374,6 +419,10 @@ function iterableEquality(a, b, customTesters = [], aStack = [], bStack = []) {
374
419
  }
375
420
  let length = aStack.length;
376
421
  while (length--) {
422
+ // Linear search. Performance is inversely proportional to the number of
423
+ // unique nested structures.
424
+ // circular references at same depth are equal
425
+ // circular reference is not equal to non-circular one
377
426
  if (aStack[length] === a) {
378
427
  return bStack[length] === b;
379
428
  }
@@ -404,6 +453,7 @@ function iterableEquality(a, b, customTesters = [], aStack = [], bStack = []) {
404
453
  }
405
454
  }
406
455
  }
456
+ // Remove the first value from the stack of traversed values.
407
457
  aStack.pop();
408
458
  bStack.pop();
409
459
  return allFound;
@@ -428,6 +478,7 @@ function iterableEquality(a, b, customTesters = [], aStack = [], bStack = []) {
428
478
  }
429
479
  }
430
480
  }
481
+ // Remove the first value from the stack of traversed values.
431
482
  aStack.pop();
432
483
  bStack.pop();
433
484
  return allFound;
@@ -446,10 +497,11 @@ function iterableEquality(a, b, customTesters = [], aStack = [], bStack = []) {
446
497
  if (!isImmutableList(a) && !isImmutableOrderedKeyed(a) && !isImmutableOrderedSet(a) && !isImmutableRecord(a)) {
447
498
  const aEntries = Object.entries(a);
448
499
  const bEntries = Object.entries(b);
449
- if (!equals(aEntries, bEntries)) {
500
+ if (!equals(aEntries, bEntries, filteredCustomTesters)) {
450
501
  return false;
451
502
  }
452
503
  }
504
+ // Remove the first value from the stack of traversed values.
453
505
  aStack.pop();
454
506
  bStack.pop();
455
507
  return true;
@@ -469,6 +521,9 @@ function isObjectWithKeys(a) {
469
521
  }
470
522
  function subsetEquality(object, subset, customTesters = []) {
471
523
  const filteredCustomTesters = customTesters.filter((t) => t !== subsetEquality);
524
+ // subsetEquality needs to keep track of the references
525
+ // it has already visited to avoid infinite loops in case
526
+ // there are circular references in the subset passed to it.
472
527
  const subsetEqualityWithContext = (seenReferences = new WeakMap()) => (object, subset) => {
473
528
  if (!isObjectWithKeys(subset)) {
474
529
  return undefined;
@@ -481,6 +536,11 @@ function subsetEquality(object, subset, customTesters = []) {
481
536
  seenReferences.set(subset[key], true);
482
537
  }
483
538
  const result = object != null && hasPropertyInObject(object, key) && equals(object[key], subset[key], [...filteredCustomTesters, subsetEqualityWithContext(seenReferences)]);
539
+ // The main goal of using seenReference is to avoid circular node on tree.
540
+ // It will only happen within a parent and its child, not a node and nodes next to it (same level)
541
+ // We should keep the reference for a parent and its child only
542
+ // Thus we should delete the reference immediately so that it doesn't interfere
543
+ // other nodes within the same level on tree.
484
544
  seenReferences.delete(subset[key]);
485
545
  return result;
486
546
  });
@@ -507,9 +567,11 @@ function arrayBufferEquality(a, b) {
507
567
  return undefined;
508
568
  }
509
569
  }
570
+ // Buffers are not equal when they do not have the same byte length
510
571
  if (dataViewA.byteLength !== dataViewB.byteLength) {
511
572
  return false;
512
573
  }
574
+ // Check if every byte value is equal to each other
513
575
  for (let i = 0; i < dataViewA.byteLength; i++) {
514
576
  if (dataViewA.getUint8(i) !== dataViewB.getUint8(i)) {
515
577
  return false;
@@ -521,6 +583,7 @@ function sparseArrayEquality(a, b, customTesters = []) {
521
583
  if (!Array.isArray(a) || !Array.isArray(b)) {
522
584
  return undefined;
523
585
  }
586
+ // A sparse array [, , 1] will have keys ["2"] whereas [undefined, undefined, 1] will have keys ["0", "1", "2"]
524
587
  const aKeys = Object.keys(a);
525
588
  const bKeys = Object.keys(b);
526
589
  const filteredCustomTesters = customTesters.filter((t) => t !== sparseArrayEquality);
@@ -547,6 +610,7 @@ function getObjectSubset(object, subset, customTesters) {
547
610
  const getObjectSubsetWithContext = (seenReferences = new WeakMap()) => (object, subset) => {
548
611
  if (Array.isArray(object)) {
549
612
  if (Array.isArray(subset) && subset.length === object.length) {
613
+ // The map method returns correct subclass of subset.
550
614
  return subset.map((sub, i) => getObjectSubsetWithContext(seenReferences)(object[i], sub));
551
615
  }
552
616
  } else if (object instanceof Date) {
@@ -557,10 +621,12 @@ function getObjectSubset(object, subset, customTesters) {
557
621
  iterableEquality,
558
622
  subsetEquality
559
623
  ])) {
624
+ // return "expected" subset to avoid showing irrelevant toMatchObject diff
560
625
  return subset;
561
626
  }
562
627
  const trimmed = {};
563
628
  seenReferences.set(object, trimmed);
629
+ // preserve constructor for toMatchObject diff
564
630
  if (typeof object.constructor === "function" && typeof object.constructor.name === "string") {
565
631
  Object.defineProperty(trimmed, "constructor", {
566
632
  enumerable: false,
@@ -614,6 +680,7 @@ function getState(expect) {
614
680
  function setState(state, expect) {
615
681
  const map = globalThis[MATCHERS_OBJECT];
616
682
  const current = map.get(expect) || {};
683
+ // so it keeps getters from `testPath`
617
684
  const results = Object.defineProperties(current, {
618
685
  ...Object.getOwnPropertyDescriptors(current),
619
686
  ...Object.getOwnPropertyDescriptors(state)
@@ -622,6 +689,7 @@ function setState(state, expect) {
622
689
  }
623
690
 
624
691
  class AsymmetricMatcher {
692
+ // should have "jest" to be compatible with its ecosystem
625
693
  $$typeof = Symbol.for("jest.asymmetricMatcher");
626
694
  constructor(sample, inverse = false) {
627
695
  this.sample = sample;
@@ -643,7 +711,12 @@ class AsymmetricMatcher {
643
711
  };
644
712
  }
645
713
  }
714
+ // implement custom chai/loupe inspect for better AssertionError.message formatting
715
+ // https://github.com/chaijs/loupe/blob/9b8a6deabcd50adc056a64fb705896194710c5c6/src/index.ts#L29
716
+ // @ts-expect-error computed properties is not supported when isolatedDeclarations is enabled
717
+ // FIXME: https://github.com/microsoft/TypeScript/issues/61068
646
718
  AsymmetricMatcher.prototype[Symbol.for("chai/inspect")] = function(options) {
719
+ // minimal pretty-format with simple manual truncation
647
720
  const result = stringify(this, options.depth, { min: true });
648
721
  if (result.length <= options.truncate) {
649
722
  return result;
@@ -872,6 +945,7 @@ const JestAsymmetricMatchers = (chai, utils) => {
872
945
  utils.addMethod(chai.expect, "arrayContaining", (expected) => new ArrayContaining(expected));
873
946
  utils.addMethod(chai.expect, "stringMatching", (expected) => new StringMatching(expected));
874
947
  utils.addMethod(chai.expect, "closeTo", (expected, precision) => new CloseTo(expected, precision));
948
+ // defineProperty does not work
875
949
  chai.expect.not = {
876
950
  stringContaining: (expected) => new StringContaining(expected, true),
877
951
  objectContaining: (expected) => new ObjectContaining(expected, true),
@@ -890,7 +964,9 @@ function createAssertionMessage(util, assertion, hasArgs) {
890
964
  }
891
965
  function recordAsyncExpect(_test, promise, assertion, error) {
892
966
  const test = _test;
967
+ // record promise for test, that resolves before test ends
893
968
  if (test && promise instanceof Promise) {
969
+ // if promise is explicitly awaited, remove it from the list
894
970
  promise = promise.finally(() => {
895
971
  if (!test.promises) {
896
972
  return;
@@ -900,6 +976,7 @@ function recordAsyncExpect(_test, promise, assertion, error) {
900
976
  test.promises.splice(index, 1);
901
977
  }
902
978
  });
979
+ // record promise
903
980
  if (!test.promises) {
904
981
  test.promises = [];
905
982
  }
@@ -937,6 +1014,7 @@ function recordAsyncExpect(_test, promise, assertion, error) {
937
1014
  }
938
1015
  function wrapAssertion(utils, name, fn) {
939
1016
  return function(...args) {
1017
+ // private
940
1018
  if (name !== "withTest") {
941
1019
  utils.flag(this, "_name", name);
942
1020
  }
@@ -959,6 +1037,7 @@ function wrapAssertion(utils, name, fn) {
959
1037
  };
960
1038
  }
961
1039
 
1040
+ // Jest Expect Compact
962
1041
  const JestChaiExpect = (chai, utils) => {
963
1042
  const { AssertionError } = chai;
964
1043
  const customTesters = getCustomEqualityTesters();
@@ -1001,6 +1080,7 @@ const JestChaiExpect = (chai, utils) => {
1001
1080
  };
1002
1081
  });
1003
1082
  });
1083
+ // @ts-expect-error @internal
1004
1084
  def("withTest", function(test) {
1005
1085
  utils.flag(this, "vitest-test", test);
1006
1086
  return this;
@@ -1091,9 +1171,11 @@ const JestChaiExpect = (chai, utils) => {
1091
1171
  const expectedClassList = isNot ? actual.value.replace(item, "").trim() : `${actual.value} ${item}`;
1092
1172
  return this.assert(actual.contains(item), `expected "${actual.value}" to contain "${item}"`, `expected "${actual.value}" not to contain "${item}"`, expectedClassList, actual.value);
1093
1173
  }
1174
+ // handle simple case on our own using `this.assert` to include diff in error message
1094
1175
  if (typeof actual === "string" && typeof item === "string") {
1095
1176
  return this.assert(actual.includes(item), `expected #{this} to contain #{exp}`, `expected #{this} not to contain #{exp}`, item, actual);
1096
1177
  }
1178
+ // make "actual" indexable to have compatibility with jest
1097
1179
  if (actual != null && typeof actual !== "string") {
1098
1180
  utils.flag(this, "object", Array.from(actual));
1099
1181
  }
@@ -1165,6 +1247,7 @@ const JestChaiExpect = (chai, utils) => {
1165
1247
  def("toHaveLength", function(length) {
1166
1248
  return this.have.length(length);
1167
1249
  });
1250
+ // destructuring, because it checks `arguments` inside, and value is passing as `undefined`
1168
1251
  def("toHaveProperty", function(...args) {
1169
1252
  if (Array.isArray(args[0])) {
1170
1253
  args[0] = args[0].map((key) => String(key).replace(/([.[\]])/g, "\\$1")).join(".");
@@ -1243,6 +1326,8 @@ const JestChaiExpect = (chai, utils) => {
1243
1326
  throw new AssertionError(msg);
1244
1327
  }
1245
1328
  });
1329
+ // manually compare array elements since `jestEquals` cannot
1330
+ // apply asymmetric matcher to `undefined` array element.
1246
1331
  function equalsArgumentArray(a, b) {
1247
1332
  return a.length === b.length && a.every((aItem, i) => equals(aItem, b[i], [...customTesters, iterableEquality]));
1248
1333
  }
@@ -1322,6 +1407,7 @@ const JestChaiExpect = (chai, utils) => {
1322
1407
  });
1323
1408
  def(["toThrow", "toThrowError"], function(expected) {
1324
1409
  if (typeof expected === "string" || typeof expected === "undefined" || expected instanceof RegExp) {
1410
+ // Fixes the issue related to `chai` <https://github.com/vitest-dev/vitest/issues/6618>
1325
1411
  return this.throws(expected === "" ? /^$/ : expected);
1326
1412
  }
1327
1413
  const obj = this._obj;
@@ -1444,14 +1530,7 @@ const JestChaiExpect = (chai, utils) => {
1444
1530
  const results = action === "return" ? spy.mock.results : spy.mock.settledResults;
1445
1531
  const result = results[results.length - 1];
1446
1532
  const spyName = spy.getMockName();
1447
- this.assert(
1448
- condition(spy, value),
1449
- `expected last "${spyName}" call to ${action} #{exp}`,
1450
- `expected last "${spyName}" call to not ${action} #{exp}`,
1451
- value,
1452
- // for jest compat
1453
- result === null || result === void 0 ? void 0 : result.value
1454
- );
1533
+ this.assert(condition(spy, value), `expected last "${spyName}" call to ${action} #{exp}`, `expected last "${spyName}" call to not ${action} #{exp}`, value, result === null || result === void 0 ? void 0 : result.value);
1455
1534
  });
1456
1535
  });
1457
1536
  [{
@@ -1478,6 +1557,7 @@ const JestChaiExpect = (chai, utils) => {
1478
1557
  this.assert(condition(spy, nthCall, value), `expected ${ordinalCall} "${spyName}" call to ${action} #{exp}`, `expected ${ordinalCall} "${spyName}" call to not ${action} #{exp}`, value, result === null || result === void 0 ? void 0 : result.value);
1479
1558
  });
1480
1559
  });
1560
+ // @ts-expect-error @internal
1481
1561
  def("withContext", function(context) {
1482
1562
  for (const key in context) {
1483
1563
  utils.flag(this, key, context[key]);
@@ -1689,6 +1769,8 @@ function JestExtendPlugin(c, expect, matchers) {
1689
1769
  value: (...sample) => new CustomMatcher(true, ...sample),
1690
1770
  writable: true
1691
1771
  });
1772
+ // keep track of asymmetric matchers on global so that it can be copied over to local context's `expect`.
1773
+ // note that the negated variant is automatically shared since it's assigned on the single `expect.not` object.
1692
1774
  Object.defineProperty(globalThis[ASYMMETRIC_MATCHERS_OBJECT], expectAssertionName, {
1693
1775
  configurable: true,
1694
1776
  enumerable: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/expect",
3
3
  "type": "module",
4
- "version": "3.2.0-beta.1",
4
+ "version": "3.2.0-beta.3",
5
5
  "description": "Jest's expect matchers as a Chai plugin",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -30,15 +30,15 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
+ "@types/chai": "^5.2.2",
33
34
  "chai": "^5.2.0",
34
35
  "tinyrainbow": "^2.0.0",
35
- "@vitest/spy": "3.2.0-beta.1",
36
- "@vitest/utils": "3.2.0-beta.1"
36
+ "@vitest/utils": "3.2.0-beta.3",
37
+ "@vitest/spy": "3.2.0-beta.3"
37
38
  },
38
39
  "devDependencies": {
39
- "@types/chai": "5.0.1",
40
40
  "rollup-plugin-copy": "^3.5.0",
41
- "@vitest/runner": "3.2.0-beta.1"
41
+ "@vitest/runner": "3.2.0-beta.3"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "rimraf dist && rollup -c",