From 9493c47894aaf63207cbd7554e86254344804f36 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:51:29 +0000 Subject: [PATCH 1/5] bun:test: take a hole of an array for undefined in toMatchObject and Bun.deepMatch `expect([, 1]).toMatchObject([undefined, 1])` failed, and its diff showed no difference. The array gate of Bun__deepMatch compared the counts of enumerable keys, and the loop after it returned false for an expected key that the received array lacks. When the key lists of two arrays differ, the gate now runs the key step of Jest's equals(). A key that only one array has is shared if it holds undefined or a matcher there and the other array has no such property. A key of the received array that is neither has to be a property of the expected array. The arrays need as many keys that are not shared. The existing loop then compares the keys of both arrays, and a key that the received array lacks reads as undefined there. The length compare, expect.objectContaining(array) and object patterns do not change. --- packages/bun-types/bun.d.ts | 4 + src/jsc/bindings/bindings.cpp | 75 ++++- test/js/bun/bun-object/deep-match.spec.ts | 7 + test/js/bun/test/expect.test.js | 262 ++++++++++++++++++ .../test/snapshot-tests/bun-snapshots.test.ts | 45 +++ test/js/bun/test/vitest-apis.test.ts | 16 ++ test/js/bun/test/vitest-flavor.test.ts | 16 +- 7 files changed, 419 insertions(+), 6 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 403e910f1b95..449a0026916b 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2937,6 +2937,10 @@ declare module "bun" { * Returns true if all properties in the subset exist in the * other and have equal values. * + * Two arrays match as a whole, not as a subset. They need the same length + * and the same own enumerable properties, with matching values. A property + * that one array lacks, such as a hole, is `undefined` there. + * * This also powers expect().toMatchObject in `bun:test` */ function deepMatch(subset: unknown, a: unknown): boolean; diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 81597bb610cc..66d18b46c0f6 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -623,6 +623,25 @@ AsymmetricMatcherResult matchAsymmetricMatcher(JSGlobalObject* globalObject, JSV return result; } +// The values that matchAsymmetricMatcherAndGetFlags() handles. A URL or a Blob has the type tag of one and is none. +static bool isAsymmetricMatcher(JSValue value) +{ + using namespace Bun::Bindgen::Generated; + if (value.isEmpty() || !value.isCell()) + return false; + JSCell* cell = value.asCell(); + if (cell->type() != JSC::JSType(JSDOMWrapperType)) + return false; + return cell->inherits() + || cell->inherits() + || cell->inherits() + || cell->inherits() + || cell->inherits() + || cell->inherits() + || cell->inherits() + || cell->inherits(); +} + template static void handlePromise(PromiseType* promise, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue ctx, Zig::FFIFunction resolverFunction, Zig::FFIFunction rejecterFunction) { @@ -744,6 +763,39 @@ static JSValue asymmetricMatchMethod(JSGlobalObject* globalObject, JSValue value return {}; } +// The key step of Jest's equals() for two arrays: a key that only one has can stand for undefined in the other, as in `[, 1]` and `[undefined, 1]`. +static NEVER_INLINE bool reconcileArrayKeys(JSGlobalObject* globalObject, ThrowScope& scope, bool withMatchers, JSObject* obj, PropertyNameArrayBuilder& objProps, JSObject* subsetObj, PropertyNameArrayBuilder& subsetProps) +{ + size_t unshared[2] = { 0, 0 }; + for (bool ofSubset : { true, false }) { + JSObject* holder = ofSubset ? subsetObj : obj; + JSObject* other = ofSubset ? obj : subsetObj; + PropertyNameArrayBuilder& from = ofSubset ? subsetProps : objProps; + PropertyNameArrayBuilder& into = ofSubset ? objProps : subsetProps; + for (size_t i = 0, count = from.size(); i < count; i++) { + // add() skips a name that the list has. The walk compares the names that subsetProps gets here. + size_t held = into.size(); + into.add(from[i]); + if (into.size() == held) + continue; + JSValue value = holder->get(globalObject, from[i]); + RETURN_IF_EXCEPTION(scope, false); + bool canBeAbsent = value.isUndefined() || (withMatchers && (isAsymmetricMatcher(value) || asymmetricMatchMethod(globalObject, value))); + bool otherOwnsIt = false; + if (canBeAbsent || !ofSubset) { + otherOwnsIt = other->hasOwnProperty(globalObject, from[i]); + RETURN_IF_EXCEPTION(scope, false); + // Jest's walk needs a key of the received array to be undefined or a matcher there, or a property of the expected array. + if (!canBeAbsent && !otherOwnsIt) + return false; + } + unshared[ofSubset] += !canBeAbsent || otherOwnsIt; + } + } + // Jest compares the counts: a key of one array can be a property of the other that isn't enumerable, and the walk compares those. + return unshared[0] == unshared[1]; +} + // Jest and Vitest treat any object with an asymmetricMatch() method as a matcher. Returns nullopt if neither or both are. static NEVER_INLINE std::optional matchUserAsymmetricMatcher(JSGlobalObject* globalObject, ThrowScope& scope, JSValue a, JSValue b) { @@ -2505,7 +2557,7 @@ bool Bun__deepMatch( // - two "simple" arrays // similar to what is done in deepEquals (canPerformFastPropertyEnumerationForIterationBun) - // arrays should match exactly + // Two arrays match as a whole, not as a subset: the keys of both are compared, and the lengths as in Vitest (Jest's toMatchObject() skips the lengths). bool objIsArray = isArray(globalObject, objValue); RETURN_IF_EXCEPTION(throwScope, false); bool subsetIsArray = objIsArray && isArray(globalObject, subsetValue); @@ -2517,16 +2569,29 @@ bool Bun__deepMatch( PropertyNameArrayBuilder objProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); obj->getPropertyNames(globalObject, objProps, DontEnumPropertiesMode::Exclude); RETURN_IF_EXCEPTION(throwScope, false); - if (objProps.size() != subsetProps.size()) { - return false; + if (isMatchingObjectContaining) { + // For expect.objectContaining(array) the two arrays need as many keys, and the walk needs each key of the sample. + if (objProps.size() != subsetProps.size()) { + return false; + } + } else if (objProps.data()->propertyNameVector() != subsetProps.data()->propertyNameVector()) [[unlikely]] { + bool canMatch = reconcileArrayKeys(globalObject, throwScope, enableAsymmetricMatchers, obj, objProps, subsetObj, subsetProps); + RETURN_IF_EXCEPTION(throwScope, false); + if (!canMatch) { + return false; + } } } for (const auto& property : subsetProps) { JSValue prop = obj->getIfPropertyExists(globalObject, property); RETURN_IF_EXCEPTION(throwScope, false); - if (prop.isEmpty()) { - return false; + if (prop.isEmpty()) [[unlikely]] { + // Between two arrays, a key that the received one lacks is undefined there. + if (!subsetIsArray || isMatchingObjectContaining) { + return false; + } + prop = jsUndefined(); } JSValue subsetProp = subsetObj->get(globalObject, property); diff --git a/test/js/bun/bun-object/deep-match.spec.ts b/test/js/bun/bun-object/deep-match.spec.ts index bef75f298447..918af965b0dd 100644 --- a/test/js/bun/bun-object/deep-match.spec.ts +++ b/test/js/bun/bun-object/deep-match.spec.ts @@ -55,6 +55,10 @@ describe("Bun.deepMatch", () => { [{}, "foo", 1], [{}, "foo", 1], ], + // NOTE: a hole is the same as `undefined`, in either array + [[, 1], [undefined, 1]], + [[undefined, 1], [, 1]], + [{ a: [[, 1]] }, { a: [[undefined, 1]], b: 2 }], // Maps [new Map(), new Map()], @@ -99,6 +103,9 @@ describe("Bun.deepMatch", () => { [[null], [undefined]], [[], [undefined]], [["a", "b", "c"], ["a", "b", "d"]], + [[, 1], [null, 1]], // NOTE: a hole is the same as nothing but `undefined` + [[0, 1], [, 1]], + [[, 1], [{}, 1]], // Maps // FIXME: I assume this is incorrect but I need confirmation on expected behavior. diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index db7fe78a3959..7fe2e10caf3a 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -3859,6 +3859,268 @@ describe("expect()", () => { different.other.back = { v: 3 }; expect(received).not.toMatchObject(different); }); + + // Jest and Vitest compare two arrays with the equals() of toEqual(), which takes a key that one array lacks for undefined. + describe("an array against an array", () => { + const withKey = (/** @type {any[]} */ array, /** @type {PropertyKey} */ key, /** @type {unknown} */ value) => + Object.assign(array, { [key]: value }); + const withGetter = (/** @type {any[]} */ array, /** @type {PropertyKey} */ key, /** @type {unknown} */ value) => + Object.defineProperty(array, key, { get: () => value, enumerable: true, configurable: true }); + const hidden = (/** @type {any[]} */ array, /** @type {PropertyKey} */ key, /** @type {unknown} */ value) => + Object.defineProperty(array, key, { value, enumerable: false, writable: true, configurable: true }); + const throwing = (/** @type {any[]} */ array, /** @type {PropertyKey} */ key) => + Object.defineProperty(array, key, { + get() { + throw new Error("thrown by the getter"); + }, + enumerable: true, + configurable: true, + }); + + test("a hole matches undefined", async () => { + expect([, 1]).toMatchObject([undefined, 1]); + expect([undefined, 1]).toMatchObject([, 1]); + expect([1, , 3]).toMatchObject([1, undefined, 3]); + expect([1, undefined]).toMatchObject([1, ,]); + expect(new Array(2)).toMatchObject([undefined, undefined]); + expect([undefined, undefined]).toMatchObject(new Array(2)); + // as many keys, and not the same keys + expect([undefined, , 1]).toMatchObject([, undefined, 1]); + // at any depth, and an object in the array is still compared as a subset + expect({ a: [, 1] }).toMatchObject({ a: [undefined, 1] }); + expect({ a: [undefined, 1] }).toMatchObject({ a: [, 1] }); + expect([[[, 1]]]).toMatchObject([[[undefined, 1]]]); + expect({ a: [{ b: [, { c: 1, d: 2 }] }] }).toMatchObject({ a: [{ b: [undefined, { c: 1 }] }] }); + // a key that is no index, and a getter + expect(withKey([1], "x", undefined)).toMatchObject([1]); + expect([1]).toMatchObject(withKey([1], "x", undefined)); + expect(withGetter([0, 1], 0, undefined)).toMatchObject([, 1]); + expect([, 1]).toMatchObject(withGetter([0, 1], 0, undefined)); + await expect(Promise.resolve([, 1])).resolves.toMatchObject([undefined, 1]); + expect(() => expect([, 1]).not.toMatchObject([undefined, 1])).toThrow(); + expect(() => expect({ a: [undefined, 1] }).not.toMatchObject({ a: [, 1] })).toThrow(); + }); + + test("a hole matches nothing else", () => { + expect([, 1]).not.toMatchObject([null, 1]); + expect([null, 1]).not.toMatchObject([, 1]); + expect([, 1]).not.toMatchObject([0, 1]); + expect([0, 1]).not.toMatchObject([, 1]); + expect([, 1]).not.toMatchObject([{}, 1]); + expect([{}, 1]).not.toMatchObject([, 1]); + expect([, 1]).not.toMatchObject([[], 1]); + expect([, 1]).not.toMatchObject([undefined, 2]); + expect([1, , 3]).not.toMatchObject([1, 2, 3]); + expect([5, , 1]).not.toMatchObject([, undefined, 1]); + expect({ a: [[, 1]] }).not.toMatchObject({ a: [[null, 1]] }); + expect([, 1]).not.toMatchObject([expect.anything(), 1]); + expect([, 1]).not.toMatchObject([expect.any(Number), 1]); + expect([, 1]).not.toMatchObject([expect.stringContaining("a"), 1]); + expect(() => expect([, 1]).toMatchObject([null, 1])).toThrow(); + }); + + test("a key that only one array has is compared", () => { + const symbol = Symbol("s"); + expect(withKey([1], "x", 1)).not.toMatchObject([1]); + expect([1]).not.toMatchObject(withKey([1], "x", 1)); + expect(withKey([1], symbol, 1)).not.toMatchObject([1]); + expect([1]).not.toMatchObject(withKey([1], symbol, 1)); + expect(withKey([1], "x", undefined)).not.toMatchObject(withKey([1], "y", 1)); + expect(withKey([1], "x", 1)).not.toMatchObject(withKey([1], "y", 1)); + expect(withKey([1], "x", { a: 1, b: 2 })).toMatchObject(withKey([1], "x", { a: 1 })); + // a name that every array inherits + expect(withKey([, 1], "at", 1)).not.toMatchObject([undefined, 1]); + expect([undefined, 1]).not.toMatchObject(withKey([, 1], "at", 1)); + expect(withKey([1, 2], "a", 1)).not.toMatchObject(withKey([1, 2], "constructor", Array)); + expect(withKey([1], "constructor", new URL("http://a/"))).not.toMatchObject([1]); + // one such name on each array + const { concat, at } = Array.prototype; + expect(withKey([, 1], "concat", concat)).not.toMatchObject(withKey([undefined, 1], "at", at)); + expect(withKey([, 1], "filter", { status: "open" })).not.toMatchObject(withKey([undefined, 1], "sort", {})); + // a property that is not enumerable is no key, but the other array can have it as a key + expect(hidden([5, 1, 2], 0, 5)).not.toMatchObject([5, 1, ,]); + expect(hidden([1], "x", 5)).not.toMatchObject(withKey([1], "x", expect.any(Number))); + const strings = ((/** @type {TemplateStringsArray} */ strings) => strings)`a${1}b`; + expect(strings).not.toMatchObject(withKey(["a", "b"], "raw", expect.any(Array))); + expect(hidden([1, 1], 0, 1)).toMatchObject(hidden([1, 1], 1, 1)); + expect(hidden(withKey([1], "a", 1), "b", 2)).toMatchObject(withKey(hidden([1], "a", 1), "b", 2)); + }); + + test("an object is still a subset", () => { + expect([, 1]).not.toMatchObject({ 0: undefined }); + expect([undefined, 1]).toMatchObject({ 0: undefined }); + expect({ a: 1 }).not.toMatchObject({ a: 1, b: undefined }); + expect({ a: [{ b: 1 }] }).not.toMatchObject({ a: [{ b: 1, c: undefined }] }); + expect({ a: [{ b: 1, c: undefined }] }).toMatchObject({ a: [{ b: 1 }] }); + }); + + test("says what toEqual() says of a value or a matcher opposite a hole", () => { + const hole = Symbol("hole"); + expect.extend({ + toBeNothing: (/** @type {unknown} */ received) => ({ pass: received === undefined, message: () => "" }), + }); + const values = [() => hole, () => undefined, () => null, () => 1]; + const matchers = [ + () => expect.anything(), + () => expect.any(Number), + () => expect.closeTo(1), + () => expect.not.stringContaining("a"), + () => expect.not.stringMatching(/a/), + () => expect.not.arrayContaining([1]), + () => expect.not.objectContaining({ a: 1 }), + // @ts-expect-error + () => expect.toBeNothing(), + () => ({ asymmetricMatch: (/** @type {unknown} */ value) => value === undefined }), + ]; + // Jest has none of these three. + if (isBun) { + matchers.push( + () => expect.not.anything(), + () => expect.not.any(Number), + () => expect.toSatisfy((/** @type {unknown} */ value) => value === undefined), + ); + } + const all = [...values, ...matchers]; + const arrayOf = (/** @type {() => unknown} */ make) => { + const array = new Array(2); + const value = make(); + if (value !== hole) array[0] = value; + array[1] = 1; + return array; + }; + const passes = (/** @type {() => void} */ assertion) => { + try { + assertion(); + return true; + } catch { + return false; + } + }; + const differ = []; + let pairs = 0; + all.forEach((received, i) => + all.forEach((expected, j) => { + // never a matcher opposite a matcher + if (i >= values.length && j >= values.length) return; + pairs++; + const equal = passes(() => expect(arrayOf(received)).toEqual(arrayOf(expected))); + const matches = passes(() => expect(arrayOf(received)).toMatchObject(arrayOf(expected))); + if (equal !== matches) differ.push({ received: i, expected: j, equal, matches }); + }), + ); + expect(pairs).toBe(isBun ? 112 : 88); + expect(differ).toEqual([]); + }); + + test("a getter of a key that only one array has is called", () => { + expect(() => expect(throwing([1], "x")).not.toMatchObject([1])).toThrow("thrown by the getter"); + expect(() => expect([1]).not.toMatchObject(throwing([1], "x"))).toThrow("thrown by the getter"); + expect(() => expect({ a: [throwing([1], "x")] }).not.toMatchObject({ a: [[1]] })).toThrow( + "thrown by the getter", + ); + }); + + // Jest 30.5.2 asks a matcher about a key that the other array lacks. Vitest 4.1.12 and 5.0.3 do not. + test_skipIf(isVitest)("a matcher opposite a hole gets undefined", () => { + /** @type {unknown[]} */ + const seen = []; + const matcher = { asymmetricMatch: (/** @type {unknown} */ value) => seen.push(value) > 0 }; + expect([, 1]).toMatchObject([expect.not.stringContaining("a"), 1]); + expect([expect.not.stringContaining("a"), 1]).toMatchObject([, 1]); + expect([1]).toMatchObject(withKey([1], "x", expect.not.stringMatching(/a/))); + expect(withKey([1], "x", expect.not.stringMatching(/a/))).toMatchObject([1]); + expect([, 1]).toMatchObject([matcher, 1]); + expect([matcher, 1]).toMatchObject([, 1]); + expect(seen).toEqual([undefined, undefined]); + // A matcher for a key that the received array inherits gets what the array inherits. + class Sized extends Array { + get size() { + return 1; + } + } + expect(Sized.of(1)).toMatchObject(withKey([1], "size", expect.closeTo(1))); + expect(Sized.of(1)).not.toMatchObject(withKey([1], "size", expect.closeTo(2))); + expect([1]).toMatchObject(withKey([1], "at", expect.anything())); + expect([1]).toMatchObject(withKey([1], "at", expect.any(Function))); + expect([1]).not.toMatchObject(withKey([1], "at", expect.any(Number))); + }); + + // Jest 30.5.2 takes a symbol key that one array lacks for undefined too. Vitest 4.1.12 and 5.0.3 do not: vitest-dev/vitest#8342. + test_skipIf(isVitest)("a symbol key that one array lacks is undefined there", () => { + const symbol = Symbol("s"); + expect(withKey([1], symbol, undefined)).toMatchObject([1]); + expect([1]).toMatchObject(withKey([1], symbol, undefined)); + }); + + // Jest 30.5.2 does not compare the lengths. Vitest 4.1.12 and 5.0.3 do, and Bun does. + test_skipIf(isJest)("the lengths are the same", () => { + expect([1, 2, 3]).not.toMatchObject([1, 2, 3, undefined]); + expect([1, undefined]).not.toMatchObject([1]); + expect(new Array(3)).not.toMatchObject([]); + expect([]).not.toMatchObject(new Array(3)); + expect(Object.assign([1, 2, 3], { length: 4 })).not.toMatchObject([1, 2, 3]); + expect({ a: [1] }).not.toMatchObject({ a: [1, undefined] }); + }); + + if (isBun) { + test("Bun.deepMatch has the same rule", () => { + expect(Bun.deepMatch([undefined, 1], [, 1])).toBe(true); + expect(Bun.deepMatch([, 1], [undefined, 1])).toBe(true); + expect(Bun.deepMatch({ a: [[, 1]] }, { a: [[undefined, 1]], b: 2 })).toBe(true); + expect(Bun.deepMatch(withKey([1], "x", undefined), [1])).toBe(true); + expect(Bun.deepMatch([null, 1], [, 1])).toBe(false); + expect(Bun.deepMatch([, 1], [0, 1])).toBe(false); + expect(Bun.deepMatch([1], withKey([1], "x", 1))).toBe(false); + expect(Bun.deepMatch([1, undefined], [1])).toBe(false); + const { concat, at } = Array.prototype; + expect(Bun.deepMatch(withKey([undefined, 1], "at", at), withKey([, 1], "concat", concat))).toBe(false); + // It has no matchers, so one is a value like any other. + expect(Bun.deepMatch([expect.not.stringContaining("a"), 1], [, 1])).toBe(false); + }); + + test("Bun.deepMatch throws what a read of a key that only one array has throws", () => { + expect(() => Bun.deepMatch(throwing([1], "x"), [1])).toThrow("thrown by the getter"); + expect(() => Bun.deepMatch([1], throwing([1], "x"))).toThrow("thrown by the getter"); + // arrays of different lengths do not match, and nothing is read + expect(Bun.deepMatch(throwing([1, 2], "x"), [1])).toBe(false); + let armed = false; + const other = new Proxy([1], { + getOwnPropertyDescriptor(target, key) { + if (armed && key === "x") throw new Error("thrown by the trap"); + return Reflect.getOwnPropertyDescriptor(target, key); + }, + }); + const holder = new Proxy(withKey([1], "x", undefined), {}); + armed = true; + expect(() => Bun.deepMatch(holder, other)).toThrow("thrown by the trap"); + }); + + // The sample of expect.objectContaining() keeps its rule: an array needs as many keys as the sample, and each key of it. + test("expect.objectContaining() of an array is not changed", () => { + expect([, 1]).not.toEqual(expect.objectContaining([undefined, 1])); + expect([undefined, , 1]).not.toEqual(expect.objectContaining([, undefined, 1])); + expect([1, 2]).toEqual(expect.objectContaining([1, 2])); + // Vitest 4.1.12 passes these two, where the array has each key of the sample and one more. + expect([undefined, 1]).not.toEqual(expect.objectContaining([, 1])); + expect(withKey([1], "x", undefined)).not.toEqual(expect.objectContaining([1])); + // an array inside is compared as toEqual() compares it + expect({ a: [, 1] }).toEqual(expect.objectContaining({ a: [undefined, 1] })); + }); + + test("the diff shows a matcher that matched a hole in its place", () => { + let message = ""; + try { + expect([, 1]).toMatchObject([expect.not.stringContaining("a"), 2]); + } catch (e) { + message = stripVTControlCharacters(ANY(e).message); + } + expect(message).toBe( + 'expect(received).toMatchObject(expected)\n\n [\n StringNotContaining "a",\n- 2,\n+ 1,\n ]\n\n- Expected - 1\n+ Received + 1\n', + ); + }); + } + }); + test("an object that occurs twice shows, in each place, what the pattern there says", () => { const number = expect.any(Number); const shared = { id: 123.5, k: 2.5 }; diff --git a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts index 41bbca2fcd5a..9de2d97b4b02 100644 --- a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts +++ b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts @@ -81,3 +81,48 @@ describe("toMatchSnapshot errors", () => { }).toThrow(); }); }); + +// Jest writes the same for these three. Vitest writes the first two, and its matcher is not asked about a hole. +test("property matchers take a hole of an array for undefined", () => { + expect({ list: [, 1], id: 5 }).toMatchInlineSnapshot( + { list: [undefined, 1], id: expect.any(Number) }, + ` + { + "id": Any, + "list": [ + undefined, + 1, + ], + } + `, + ); + expect({ list: [undefined, 1] }).toMatchInlineSnapshot( + { list: [, 1] }, + ` + { + "list": [ + undefined, + 1, + ], + } + `, + ); + expect({ list: [, 1, { a: 1, b: 2 }] }).toMatchInlineSnapshot( + { list: [expect.not.stringContaining("a"), 1, { a: expect.any(Number) }] }, + ` + { + "list": [ + StringNotContaining "a", + 1, + { + "a": Any, + "b": 2, + }, + ], + } + `, + ); + expect(() => expect({ list: [, 1] }).toMatchSnapshot({ list: [null, 1] })).toThrow( + "to match properties from received object", + ); +}); diff --git a/test/js/bun/test/vitest-apis.test.ts b/test/js/bun/test/vitest-apis.test.ts index 1e8221e52ca1..e5c8f229c8ef 100644 --- a/test/js/bun/test/vitest-apis.test.ts +++ b/test/js/bun/test/vitest-apis.test.ts @@ -1023,6 +1023,22 @@ describe("expect.addEqualityTesters", () => { expect(seen.map(([, a, b]) => [a, b]).slice(1)).toEqual([[undefined, 2]]); }); + test("a tester has no say about a key that only one of two arrays has in toMatchObject", () => { + const nothing = 192837465; + const isNothing = (value: unknown) => value === undefined || value === nothing; + expect.addEqualityTesters([ + (a, b) => ((a === nothing || b === nothing) && isNothing(a) && isNothing(b) ? true : undefined), + ]); + // Where both arrays have the key, the tester takes the marker for undefined. + expect([nothing, 1]).toMatchObject([undefined, 1]); + expect([undefined, 1]).toMatchObject([nothing, 1]); + // A hole is no key. The marker is not undefined, so the arrays do not match, as in Jest and Vitest. + expect([nothing, 1]).not.toMatchObject([, 1]); + expect([, 1]).not.toMatchObject([nothing, 1]); + expect([nothing, , 1]).not.toMatchObject([, nothing, 1]); + expect(Object.assign([1], { x: nothing })).not.toMatchObject(Object.assign([1], { y: nothing })); + }); + test("asymmetric matchers come first", () => { seen.length = 0; expect(litre()).toEqual(expect.any(Volume)); diff --git a/test/js/bun/test/vitest-flavor.test.ts b/test/js/bun/test/vitest-flavor.test.ts index c343e3008134..d04c37679880 100644 --- a/test/js/bun/test/vitest-flavor.test.ts +++ b/test/js/bun/test/vitest-flavor.test.ts @@ -5363,6 +5363,14 @@ test.concurrent("arrays of different lengths can be equal in Jest and never in V "a hole": () => expect([, "a"]).toEqual([undefined, "a"]), "an undefined property": () => expect({ a: 1 }).toEqual({ a: 1, b: undefined }), "Bun.deepEquals": () => expect(Bun.deepEquals(["a"], ["a", undefined])).toBe(true), + // toMatchObject() compares the lengths in a test of either, and takes a hole for undefined in both. + "toMatchObject": () => expect(["a"]).toMatchObject(["a", undefined]), + "toMatchObject, a hole": () => expect([, "a"]).toMatchObject([undefined, "a"]), + // Vitest does not ask a matcher about a hole. Bun asks it in a test of either, as Jest does. + "a matcher opposite a hole": () => { + expect([, "a"]).toEqual([expect.not.stringContaining("b"), "a"]); + expect([, "a"]).toMatchObject([expect.not.stringContaining("b"), "a"]); + }, }; test("checks", () => { for (const [name, check] of Object.entries(checks)) { @@ -5381,7 +5389,13 @@ test.concurrent("arrays of different lengths can be equal in Jest and never in V runTests({ "a.test.js": checks, "bunfig.toml": `[test]\nglobals = "vitest"\n` }), runTests({ "a.test.js": ofVitest, "b.test.js": ofBun }), ]); - const inBoth = ["a hole", "an undefined property", "Bun.deepEquals"]; + const inBoth = [ + "a hole", + "an undefined property", + "Bun.deepEquals", + "toMatchObject, a hole", + "a matcher opposite a hole", + ]; const inJest = [ "toEqual", "nested", From a21b532595a202e3c06ef4549e8957a2fa3f64d8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:38:27 +0000 Subject: [PATCH 2/5] bun-types: say which properties of two arrays Bun.deepMatch compares --- packages/bun-types/bun.d.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 449a0026916b..faa91c1da76f 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2937,9 +2937,10 @@ declare module "bun" { * Returns true if all properties in the subset exist in the * other and have equal values. * - * Two arrays match as a whole, not as a subset. They need the same length - * and the same own enumerable properties, with matching values. A property - * that one array lacks, such as a hole, is `undefined` there. + * Two arrays match as a whole, not as a subset. They need the same length, + * and each enumerable property of one array has to match the same property + * of the other. A property that one array lacks, such as a hole, is + * `undefined` there. * * This also powers expect().toMatchObject in `bun:test` */ From 27c9b6c0a365d29b2298c3fc70adbfd4f6d35601 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:12:38 +0000 Subject: [PATCH 3/5] bun:test: use Expect::is_asymmetric_matcher in the array key step of toMatchObject reconcileArrayKeys() had a list of the matcher classes of its own. It now calls the predicate of expect.rs through Expect_isAsymmetricMatcher. The tests name the message of a failed toMatchObject() where they only checked for a throw. --- src/jsc/bindings/bindings.cpp | 22 ++-------------------- src/runtime/test_runner/expect.rs | 8 +++++++- test/js/bun/test/expect.test.js | 10 ++++++---- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 66d18b46c0f6..a0056bfd8eb8 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -268,6 +268,7 @@ template bool Bun__deepMatch(JSValue objValue, JSValue subsetValue, JSGlob template bool Bun__deepMatch(JSValue objValue, JSValue subsetValue, JSGlobalObject* globalObject, ThrowScope& throwScope, Bun::DeepMatchState& state, bool isMatchingObjectContaining); extern "C" bool Expect_readFlagsAndProcessPromise(JSC::EncodedJSValue instanceValue, JSC::JSGlobalObject* globalObject, ExpectFlags* flags, JSC::EncodedJSValue* value, AsymmetricMatcherConstructorType* constructorType); +extern "C" bool Expect_isAsymmetricMatcher(JSC::EncodedJSValue value); extern "C" int8_t AsymmetricMatcherConstructorType__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue) { @@ -623,25 +624,6 @@ AsymmetricMatcherResult matchAsymmetricMatcher(JSGlobalObject* globalObject, JSV return result; } -// The values that matchAsymmetricMatcherAndGetFlags() handles. A URL or a Blob has the type tag of one and is none. -static bool isAsymmetricMatcher(JSValue value) -{ - using namespace Bun::Bindgen::Generated; - if (value.isEmpty() || !value.isCell()) - return false; - JSCell* cell = value.asCell(); - if (cell->type() != JSC::JSType(JSDOMWrapperType)) - return false; - return cell->inherits() - || cell->inherits() - || cell->inherits() - || cell->inherits() - || cell->inherits() - || cell->inherits() - || cell->inherits() - || cell->inherits(); -} - template static void handlePromise(PromiseType* promise, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue ctx, Zig::FFIFunction resolverFunction, Zig::FFIFunction rejecterFunction) { @@ -780,7 +762,7 @@ static NEVER_INLINE bool reconcileArrayKeys(JSGlobalObject* globalObject, ThrowS continue; JSValue value = holder->get(globalObject, from[i]); RETURN_IF_EXCEPTION(scope, false); - bool canBeAbsent = value.isUndefined() || (withMatchers && (isAsymmetricMatcher(value) || asymmetricMatchMethod(globalObject, value))); + bool canBeAbsent = value.isUndefined() || (withMatchers && (Expect_isAsymmetricMatcher(JSValue::encode(value)) || asymmetricMatchMethod(globalObject, value))); bool otherOwnsIt = false; if (canBeAbsent || !ofSubset) { otherOwnsIt = other->hasOwnProperty(globalObject, from[i]); diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index f24aacfd29cb..7b1391d113ae 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -620,6 +620,12 @@ impl Expect { false } + /// Called by C++ to ask about a value without running a matcher + #[unsafe(no_mangle)] + pub(crate) extern "C" fn Expect_isAsymmetricMatcher(value: JSValue) -> bool { + Self::is_asymmetric_matcher(value) + } + /// Called by C++ when matching with asymmetric matchers #[unsafe(no_mangle)] pub(crate) extern "C" fn Expect_readFlagsAndProcessPromise( @@ -3428,7 +3434,7 @@ unsafe extern "C" { } // Exports: handled by #[unsafe(no_mangle)] on: -// ExpectMatcherUtils_createSigleton, Expect_readFlagsAndProcessPromise, ExpectCustomAsymmetricMatcher__execute +// ExpectMatcherUtils_createSigleton, Expect_isAsymmetricMatcher, Expect_readFlagsAndProcessPromise, ExpectCustomAsymmetricMatcher__execute #[cfg(test)] mod tests { diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 7fe2e10caf3a..170411f7b4d6 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -3876,6 +3876,8 @@ describe("expect()", () => { enumerable: true, configurable: true, }); + // what toMatchObject() throws when it fails, in Jest and Bun, and in Vitest + const mismatch = /toMatchObject|match object/; test("a hole matches undefined", async () => { expect([, 1]).toMatchObject([undefined, 1]); @@ -3897,8 +3899,8 @@ describe("expect()", () => { expect(withGetter([0, 1], 0, undefined)).toMatchObject([, 1]); expect([, 1]).toMatchObject(withGetter([0, 1], 0, undefined)); await expect(Promise.resolve([, 1])).resolves.toMatchObject([undefined, 1]); - expect(() => expect([, 1]).not.toMatchObject([undefined, 1])).toThrow(); - expect(() => expect({ a: [undefined, 1] }).not.toMatchObject({ a: [, 1] })).toThrow(); + expect(() => expect([, 1]).not.toMatchObject([undefined, 1])).toThrow(mismatch); + expect(() => expect({ a: [undefined, 1] }).not.toMatchObject({ a: [, 1] })).toThrow(mismatch); }); test("a hole matches nothing else", () => { @@ -3916,7 +3918,7 @@ describe("expect()", () => { expect([, 1]).not.toMatchObject([expect.anything(), 1]); expect([, 1]).not.toMatchObject([expect.any(Number), 1]); expect([, 1]).not.toMatchObject([expect.stringContaining("a"), 1]); - expect(() => expect([, 1]).toMatchObject([null, 1])).toThrow(); + expect(() => expect([, 1]).toMatchObject([null, 1])).toThrow(mismatch); }); test("a key that only one array has is compared", () => { @@ -4100,7 +4102,7 @@ describe("expect()", () => { expect([, 1]).not.toEqual(expect.objectContaining([undefined, 1])); expect([undefined, , 1]).not.toEqual(expect.objectContaining([, undefined, 1])); expect([1, 2]).toEqual(expect.objectContaining([1, 2])); - // Vitest 4.1.12 passes these two, where the array has each key of the sample and one more. + // Vitest 4.1.12 and 5.0.3 pass these two, where the array has each key of the sample and one more. Jest 30.5.2 fails them: its ObjectContaining matches no array. expect([undefined, 1]).not.toEqual(expect.objectContaining([, 1])); expect(withKey([1], "x", undefined)).not.toEqual(expect.objectContaining([1])); // an array inside is compared as toEqual() compares it From f4a307fc4ab7747e0a347a7c33a16e294e0a91dd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:05:14 +0000 Subject: [PATCH 4/5] ci: retrigger From fab0b1334b4cf7693fc1aaa97fd0228b62079225 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:09:50 +0000 Subject: [PATCH 5/5] bun:test: keep the rule of main for a private name in the array key step After the merge with main, plain Bun and code compiled ahead of time gave different answers for two arrays when only one of them has a private field that holds undefined. The key step took the private name for a key that the other array may lack. An executable lists no private field and counts them, so it did not match the pair. A private name that only one array has now ends the key step with no match, as on main. A new project under test/aot/parallel-test-runs runs the rows in both modes. Tests: the checks of the two test modules and of an equality tester run in a child of expect.test.js, beside the other tests of two arrays. The custom matcher opposite a hole is asserted directly. --- src/jsc/bindings/bindings.cpp | 3 + .../sees_test.mjs | 22 +++++ test/js/bun/test/expect.test.js | 94 +++++++++++++++++++ test/js/bun/test/vitest-apis.test.ts | 16 ---- test/js/bun/test/vitest-flavor.test.ts | 16 +--- 5 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 test/aot/parallel-test-runs/what-a-comparison-finds-in-private-fields/a-private-field-that-one-list-alone-has/sees_test.mjs diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 876bbc427fb0..512bc5a3ad3b 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -784,6 +784,9 @@ static NEVER_INLINE bool reconcileArrayKeys(JSGlobalObject* globalObject, ThrowS into.add(from[i]); if (into.size() == held) continue; + // A private name is not reconciled: both arrays need it. Code compiled ahead of time lists no private field, and counts them to the same end. + if (from[i].isPrivateName()) + return false; JSValue value = holder->get(globalObject, from[i]); RETURN_IF_EXCEPTION(scope, false); bool canBeAbsent = value.isUndefined() || (withMatchers && (Expect_isAsymmetricMatcher(JSValue::encode(value)) || asymmetricMatchMethod(globalObject, value))); diff --git a/test/aot/parallel-test-runs/what-a-comparison-finds-in-private-fields/a-private-field-that-one-list-alone-has/sees_test.mjs b/test/aot/parallel-test-runs/what-a-comparison-finds-in-private-fields/a-private-field-that-one-list-alone-has/sees_test.mjs new file mode 100644 index 000000000000..0807c9fa4cd3 --- /dev/null +++ b/test/aot/parallel-test-runs/what-a-comparison-finds-in-private-fields/a-private-field-that-one-list-alone-has/sees_test.mjs @@ -0,0 +1,22 @@ +// Guards: two lists match as a whole, and a key that one of them lacks stands for undefined there. A private field is no such key: both lists need it. Plain Bun took a private field that holds undefined for a key that the other list may lack and passed the pair, and an executable, which counts private fields, failed it. +import { expect, test } from "bun:test"; +class Bare extends Array {} +class Unset extends Array { #kept; } +class Other extends Array { #other; } +class Two extends Array { #first; #second; } +class Held extends Array { #kept; static of(kept, ...items) { const list = super.of(...items); list.#kept = kept; return list; } } +const says = check => { try { return check() === false ? "false" : "passes"; } catch (error) { return error.constructor.name; } }; +const holed = list => { delete list[0]; return list; }; +test("one list alone has the field", () => { + console.log(says(() => expect(Unset.of(1)).toMatchObject([1])), says(() => expect([1]).toMatchObject(Unset.of(1))), says(() => expect(Unset.of(1)).toMatchObject(Bare.of(1))), says(() => expect(Unset.of(1)).toMatchObject(Other.of(1))), says(() => expect(Two.of(1)).toMatchObject(Unset.of(1))), says(() => expect(Unset.of(1)).toMatchObject(Two.of(1)))); + console.log(says(() => expect(holed(Unset.of(0, 1))).toMatchObject([undefined, 1])), says(() => expect([undefined, 1]).toMatchObject(holed(Unset.of(0, 1)))), says(() => expect(holed(Unset.of(0, 1))).toMatchObject(Other.of(undefined, 1)))); + console.log(says(() => Bun.deepMatch([1], Unset.of(1))), says(() => Bun.deepMatch(Unset.of(1), [1])), says(() => Bun.deepMatch(Unset.of(1), Other.of(1))), says(() => Bun.deepMatch([undefined, 1], holed(Unset.of(0, 1))))); +}); +test("both lists have the field", () => { + console.log(says(() => expect(Held.of(1, 1)).toMatchObject(Held.of(1, 1))), says(() => expect(Held.of(2, 1)).toMatchObject(Held.of(1, 1))), says(() => expect(Held.of(1, 1)).toMatchObject(Held.of(expect.any(Number), 1)))); + console.log(says(() => expect(holed(Held.of(1, 0, 1))).toMatchObject(Held.of(1, undefined, 1))), says(() => expect(Held.of(1, undefined, 1)).toMatchObject(holed(Held.of(1, 0, 1)))), says(() => expect(holed(Held.of(2, 0, 1))).toMatchObject(Held.of(1, undefined, 1)))); + console.log(says(() => Bun.deepMatch(Held.of(1, undefined, 1), holed(Held.of(1, 0, 1)))), says(() => Bun.deepMatch(Held.of(2, undefined, 1), holed(Held.of(1, 0, 1))))); +}); +test("objectContaining", () => { + console.log(says(() => expect(Unset.of(1)).toEqual(expect.objectContaining([1]))), says(() => expect(Unset.of(1)).toEqual(expect.objectContaining(Unset.of(1)))), says(() => expect(Unset.of(1)).toEqual(expect.objectContaining(Other.of(1)))), says(() => expect(holed(Unset.of(0, 1))).toEqual(expect.objectContaining(Unset.of(undefined, 1))))); +}); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 170411f7b4d6..0b560c023adc 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -4012,6 +4012,13 @@ describe("expect()", () => { ); expect(pairs).toBe(isBun ? 112 : 88); expect(differ).toEqual([]); + // The custom matcher is asked and passes, so its pairs above are not two errors that agree. Vitest does not ask it. + if (!isVitest) { + // @ts-expect-error + expect([, 1]).toEqual([expect.toBeNothing(), 1]); + // @ts-expect-error + expect([, 1]).toMatchObject([expect.toBeNothing(), 1]); + } }); test("a getter of a key that only one array has is called", () => { @@ -4120,6 +4127,93 @@ describe("expect()", () => { 'expect(received).toMatchObject(expected)\n\n [\n StringNotContaining "a",\n- 2,\n+ 1,\n ]\n\n- Expected - 1\n+ Received + 1\n', ); }); + + // Jest does not compare the lengths of two arrays here, and Vitest asks no matcher about a hole. Bun gives one answer in a test of either module. + test("does not depend on the module of the test", async () => { + const { bunEnv, bunExe, tempDir } = require("harness"); + const checks = (/** @type {string} */ module) => ` + import { expect, test } from "${module}"; + const says = check => { try { check(); return "passes"; } catch { return "fails"; } }; + test("checks", () => { + console.log(JSON.stringify({ + "toEqual() of a longer array": says(() => expect(["a"]).toEqual(["a", undefined])), + "a hole": says(() => expect([, "a"]).toMatchObject([undefined, "a"])), + "a longer array": says(() => expect(["a"]).toMatchObject(["a", undefined])), + "a matcher opposite a hole": says(() => expect([, "a"]).toMatchObject([expect.not.stringContaining("b"), "a"])), + })); + }); + `; + using dir = tempDir("expect-array-against-array", { + "of-bun.test.js": checks("bun:test"), + "of-vitest.test.js": checks("vitest"), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // toEqual() does depend on the module, which shows that the two files ran as tests of two modules. + const answers = (/** @type {string} */ toEqual) => ({ + "toEqual() of a longer array": toEqual, + "a hole": "passes", + "a longer array": "fails", + "a matcher opposite a hole": "passes", + }); + expect({ + answers: stdout + .split("\n") + .filter(line => line.startsWith("{")) + .map(line => JSON.parse(line)), + stderr, + exitCode, + }).toMatchObject({ answers: [answers("passes"), answers("fails")], exitCode: 0 }); + }); + + // A tester lasts until the end of its test file, so this one is added in a child. + test("a tester has no say about a key that only one array has", async () => { + const { bunEnv, bunExe } = require("harness"); + const src = ` + const { expect } = require("bun:test"); + const nothing = 192837465; + const isNothing = value => value === undefined || value === nothing; + expect.addEqualityTesters([ + (a, b) => ((a === nothing || b === nothing) && isNothing(a) && isNothing(b) ? true : undefined), + ]); + const says = check => { try { check(); return "passes"; } catch { return "fails"; } }; + console.log(JSON.stringify({ + "the marker, undefined": says(() => expect([nothing, 1]).toMatchObject([undefined, 1])), + "undefined, the marker": says(() => expect([undefined, 1]).toMatchObject([nothing, 1])), + "the marker, a hole": says(() => expect([nothing, 1]).toMatchObject([, 1])), + "a hole, the marker": says(() => expect([, 1]).toMatchObject([nothing, 1])), + "the marker and a hole, crossed": says(() => expect([nothing, , 1]).toMatchObject([, nothing, 1])), + "the marker under two names": says(() => + expect(Object.assign([1], { x: nothing })).toMatchObject(Object.assign([1], { y: nothing })), + ), + })); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // Where both arrays have the key, the tester takes the marker for undefined. A hole is no key, as in Jest and Vitest. + expect({ answers: stdout && JSON.parse(stdout), stderr, exitCode }).toMatchObject({ + answers: { + "the marker, undefined": "passes", + "undefined, the marker": "passes", + "the marker, a hole": "fails", + "a hole, the marker": "fails", + "the marker and a hole, crossed": "fails", + "the marker under two names": "fails", + }, + exitCode: 0, + }); + }); } }); diff --git a/test/js/bun/test/vitest-apis.test.ts b/test/js/bun/test/vitest-apis.test.ts index e5c8f229c8ef..1e8221e52ca1 100644 --- a/test/js/bun/test/vitest-apis.test.ts +++ b/test/js/bun/test/vitest-apis.test.ts @@ -1023,22 +1023,6 @@ describe("expect.addEqualityTesters", () => { expect(seen.map(([, a, b]) => [a, b]).slice(1)).toEqual([[undefined, 2]]); }); - test("a tester has no say about a key that only one of two arrays has in toMatchObject", () => { - const nothing = 192837465; - const isNothing = (value: unknown) => value === undefined || value === nothing; - expect.addEqualityTesters([ - (a, b) => ((a === nothing || b === nothing) && isNothing(a) && isNothing(b) ? true : undefined), - ]); - // Where both arrays have the key, the tester takes the marker for undefined. - expect([nothing, 1]).toMatchObject([undefined, 1]); - expect([undefined, 1]).toMatchObject([nothing, 1]); - // A hole is no key. The marker is not undefined, so the arrays do not match, as in Jest and Vitest. - expect([nothing, 1]).not.toMatchObject([, 1]); - expect([, 1]).not.toMatchObject([nothing, 1]); - expect([nothing, , 1]).not.toMatchObject([, nothing, 1]); - expect(Object.assign([1], { x: nothing })).not.toMatchObject(Object.assign([1], { y: nothing })); - }); - test("asymmetric matchers come first", () => { seen.length = 0; expect(litre()).toEqual(expect.any(Volume)); diff --git a/test/js/bun/test/vitest-flavor.test.ts b/test/js/bun/test/vitest-flavor.test.ts index d04c37679880..c343e3008134 100644 --- a/test/js/bun/test/vitest-flavor.test.ts +++ b/test/js/bun/test/vitest-flavor.test.ts @@ -5363,14 +5363,6 @@ test.concurrent("arrays of different lengths can be equal in Jest and never in V "a hole": () => expect([, "a"]).toEqual([undefined, "a"]), "an undefined property": () => expect({ a: 1 }).toEqual({ a: 1, b: undefined }), "Bun.deepEquals": () => expect(Bun.deepEquals(["a"], ["a", undefined])).toBe(true), - // toMatchObject() compares the lengths in a test of either, and takes a hole for undefined in both. - "toMatchObject": () => expect(["a"]).toMatchObject(["a", undefined]), - "toMatchObject, a hole": () => expect([, "a"]).toMatchObject([undefined, "a"]), - // Vitest does not ask a matcher about a hole. Bun asks it in a test of either, as Jest does. - "a matcher opposite a hole": () => { - expect([, "a"]).toEqual([expect.not.stringContaining("b"), "a"]); - expect([, "a"]).toMatchObject([expect.not.stringContaining("b"), "a"]); - }, }; test("checks", () => { for (const [name, check] of Object.entries(checks)) { @@ -5389,13 +5381,7 @@ test.concurrent("arrays of different lengths can be equal in Jest and never in V runTests({ "a.test.js": checks, "bunfig.toml": `[test]\nglobals = "vitest"\n` }), runTests({ "a.test.js": ofVitest, "b.test.js": ofBun }), ]); - const inBoth = [ - "a hole", - "an undefined property", - "Bun.deepEquals", - "toMatchObject, a hole", - "a matcher opposite a hole", - ]; + const inBoth = ["a hole", "an undefined property", "Bun.deepEquals"]; const inJest = [ "toEqual", "nested",