From 5f6be286f5135c6c238994f63016419e6301d73f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 00:14:49 +0000 Subject: [PATCH 01/26] bun:test: don't block on pending promises in expect().resolves/.rejects expect(promise).resolves.() used to call waitForPromise(), synchronously spinning the event loop until the promise settled. If the only thing that could settle it was JS still on the call stack above the matcher (e.g. the next statement in the test function), the test hung at 100% CPU. Now, when the input promise is still pending, getValue() attaches a .then() handler and returns a new pending Promise from the matcher call. When the input settles, the matcher is re-invoked (with the now-settled promise) and the returned Promise is resolved/rejected with the outcome. This matches Jest's non-blocking behavior. Already-settled promises still take the synchronous path, so existing behavior is preserved for the common case. Fixes #14950 --- src/jsc/bindings/ZigGlobalObject.cpp | 4 + src/jsc/bindings/ZigGlobalObject.h | 4 +- src/jsc/bindings/headers.h | 2 + src/runtime/test_runner/expect.zig | 155 +++++++++++++- src/runtime/test_runner/expect/toBe.zig | 2 +- src/runtime/test_runner/expect/toBeArray.zig | 2 +- .../test_runner/expect/toBeArrayOfSize.zig | 2 +- .../test_runner/expect/toBeBoolean.zig | 2 +- .../test_runner/expect/toBeCloseTo.zig | 2 +- src/runtime/test_runner/expect/toBeDate.zig | 2 +- .../test_runner/expect/toBeDefined.zig | 2 +- src/runtime/test_runner/expect/toBeEmpty.zig | 2 +- .../test_runner/expect/toBeEmptyObject.zig | 2 +- src/runtime/test_runner/expect/toBeEven.zig | 2 +- src/runtime/test_runner/expect/toBeFalse.zig | 2 +- src/runtime/test_runner/expect/toBeFalsy.zig | 2 +- src/runtime/test_runner/expect/toBeFinite.zig | 2 +- .../test_runner/expect/toBeFunction.zig | 2 +- .../test_runner/expect/toBeGreaterThan.zig | 2 +- .../expect/toBeGreaterThanOrEqual.zig | 2 +- .../test_runner/expect/toBeInstanceOf.zig | 2 +- .../test_runner/expect/toBeInteger.zig | 2 +- .../test_runner/expect/toBeLessThan.zig | 2 +- .../expect/toBeLessThanOrEqual.zig | 2 +- src/runtime/test_runner/expect/toBeNaN.zig | 2 +- .../test_runner/expect/toBeNegative.zig | 2 +- src/runtime/test_runner/expect/toBeNil.zig | 2 +- src/runtime/test_runner/expect/toBeNull.zig | 2 +- src/runtime/test_runner/expect/toBeNumber.zig | 2 +- src/runtime/test_runner/expect/toBeObject.zig | 2 +- src/runtime/test_runner/expect/toBeOdd.zig | 2 +- src/runtime/test_runner/expect/toBeOneOf.zig | 2 +- .../test_runner/expect/toBePositive.zig | 2 +- src/runtime/test_runner/expect/toBeString.zig | 2 +- src/runtime/test_runner/expect/toBeSymbol.zig | 2 +- src/runtime/test_runner/expect/toBeTrue.zig | 2 +- src/runtime/test_runner/expect/toBeTruthy.zig | 2 +- src/runtime/test_runner/expect/toBeTypeOf.zig | 2 +- .../test_runner/expect/toBeUndefined.zig | 2 +- .../test_runner/expect/toBeValidDate.zig | 2 +- src/runtime/test_runner/expect/toBeWithin.zig | 2 +- src/runtime/test_runner/expect/toContain.zig | 2 +- .../test_runner/expect/toContainAllKeys.zig | 2 +- .../test_runner/expect/toContainAllValues.zig | 2 +- .../test_runner/expect/toContainAnyKeys.zig | 2 +- .../test_runner/expect/toContainAnyValues.zig | 2 +- .../test_runner/expect/toContainEqual.zig | 2 +- .../test_runner/expect/toContainKey.zig | 2 +- .../test_runner/expect/toContainKeys.zig | 2 +- .../test_runner/expect/toContainValue.zig | 2 +- .../test_runner/expect/toContainValues.zig | 2 +- src/runtime/test_runner/expect/toEndWith.zig | 2 +- src/runtime/test_runner/expect/toEqual.zig | 2 +- .../expect/toEqualIgnoringWhitespace.zig | 2 +- .../test_runner/expect/toHaveBeenCalled.zig | 2 +- .../expect/toHaveBeenCalledOnce.zig | 2 +- .../expect/toHaveBeenCalledTimes.zig | 2 +- .../expect/toHaveBeenCalledWith.zig | 2 +- .../expect/toHaveBeenLastCalledWith.zig | 2 +- .../expect/toHaveBeenNthCalledWith.zig | 2 +- .../expect/toHaveLastReturnedWith.zig | 2 +- .../test_runner/expect/toHaveLength.zig | 2 +- .../expect/toHaveNthReturnedWith.zig | 2 +- .../test_runner/expect/toHaveProperty.zig | 2 +- .../test_runner/expect/toHaveReturned.zig | 2 +- .../test_runner/expect/toHaveReturnedWith.zig | 2 +- src/runtime/test_runner/expect/toInclude.zig | 2 +- src/runtime/test_runner/expect/toMatch.zig | 2 +- .../expect/toMatchInlineSnapshot.zig | 2 +- .../test_runner/expect/toMatchObject.zig | 2 +- .../test_runner/expect/toMatchSnapshot.zig | 2 +- .../test_runner/expect/toStartWith.zig | 2 +- .../test_runner/expect/toStrictEqual.zig | 2 +- src/runtime/test_runner/expect/toThrow.zig | 3 +- .../toThrowErrorMatchingInlineSnapshot.zig | 3 +- .../expect/toThrowErrorMatchingSnapshot.zig | 3 +- .../bun/test/expect-resolves-pending.test.ts | 198 ++++++++++++++++++ 77 files changed, 434 insertions(+), 76 deletions(-) create mode 100644 test/js/bun/test/expect-resolves-pending.test.ts diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 456898e72aa..30b9accca77 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3872,6 +3872,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::Bun__HTTPRequestContextDebugH3__onResolve; } else if (handler == Bun__HTTPRequestContextDebugH3__onResolveStream) { return GlobalObject::PromiseFunctions::Bun__HTTPRequestContextDebugH3__onResolveStream; + } else if (handler == Bun__Expect__PendingMatcher__onResolve) { + return GlobalObject::PromiseFunctions::Bun__Expect__PendingMatcher__onResolve; + } else if (handler == Bun__Expect__PendingMatcher__onReject) { + return GlobalObject::PromiseFunctions::Bun__Expect__PendingMatcher__onReject; } else { RELEASE_ASSERT_NOT_REACHED(); } diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index eae5d1cc5a6..2a2f1ea5ee9 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -412,8 +412,10 @@ class GlobalObject : public Bun::GlobalScope { Bun__HTTPRequestContextDebugH3__onRejectStream, Bun__HTTPRequestContextDebugH3__onResolve, Bun__HTTPRequestContextDebugH3__onResolveStream, + Bun__Expect__PendingMatcher__onResolve, + Bun__Expect__PendingMatcher__onReject, }; - static constexpr size_t promiseFunctionsSize = 42; + static constexpr size_t promiseFunctionsSize = 44; static PromiseFunctions promiseHandlerID(SYSV_ABI EncodedJSValue (*handler)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); diff --git a/src/jsc/bindings/headers.h b/src/jsc/bindings/headers.h index c85e7379065..97a073600cb 100644 --- a/src/jsc/bindings/headers.h +++ b/src/jsc/bindings/headers.h @@ -787,6 +787,8 @@ BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__Describe2__bunTestThen); BUN_DECLARE_HOST_FUNCTION(Bun__TestScope__Describe2__bunTestCatch); BUN_DECLARE_HOST_FUNCTION(Bun__CronJob__onPromiseResolve); BUN_DECLARE_HOST_FUNCTION(Bun__CronJob__onPromiseReject); +BUN_DECLARE_HOST_FUNCTION(Bun__Expect__PendingMatcher__onResolve); +BUN_DECLARE_HOST_FUNCTION(Bun__Expect__PendingMatcher__onReject); #endif diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index 8f3cda8d855..d3d862e5ec8 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -24,6 +24,10 @@ pub const Expect = struct { flags: Flags = .{}, parent: ?*bun.jsc.Jest.bun_test.BunTest.RefData, custom_label: bun.String = bun.String.empty, + /// Set to true while re-invoking a matcher from a `.resolves`/`.rejects` + /// `.then()` callback so we don't double-count the expectation or defer + /// again. + is_async_rerun: bool = false, pub const TestScope = struct { test_id: TestRunner.Test.ID, @@ -31,6 +35,7 @@ pub const Expect = struct { }; pub fn incrementExpectCallCounter(this: *Expect) void { + if (this.is_async_rerun) return; // already counted on the first (deferred) call const parent = this.parent orelse return; // not in bun:test var buntest_strong = parent.bunTest() orelse return; // the test file this expect() call was for is no longer defer buntest_strong.deinit(); @@ -158,17 +163,158 @@ pub const Expect = struct { return thisValue; } - pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, matcher_name: string, comptime matcher_params_fmt: string) bun.JSError!JSValue { + /// Retrieves the captured value passed to `expect(...)`, processing + /// `.resolves`/`.rejects` if set. + /// + /// Returns `null` when the captured value is a still-pending promise. + /// In that case the matcher is deferred: a `.then()` is attached that + /// re-invokes the matcher once the promise settles, and the matcher + /// must immediately return the promise from `deferredResult()`. + pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, matcher_name: string, comptime matcher_params_fmt: string) bun.JSError!?JSValue { const value = js.capturedValueGetCached(thisValue) orelse { return globalThis.throw("Internal error: the expect(value) was garbage collected but it should not have been!", .{}); }; value.ensureStillAlive(); + if (try this.maybeDeferMatcher(globalThis, thisValue, callframe, value)) |_| { + return null; + } + const matcher_params = switch (Output.enable_ansi_colors_stderr) { inline else => |colors| comptime Output.prettyFmt(matcher_params_fmt, colors), }; - return processPromise(this.custom_label, this.flags, globalThis, value, matcher_name, matcher_params, false); - } + return try processPromise(this.custom_label, this.flags, globalThis, value, matcher_name, matcher_params, false); + } + + /// Returns the promise that a deferred matcher should return to its caller. + /// Only valid when `getValue()` returned `null`. + pub fn deferredResult(_: *Expect, thisValue: JSValue) JSValue { + return js.resultValueGetCached(thisValue) orelse .js_undefined; + } + + /// If `.resolves`/`.rejects` is set and `value` is a still-pending promise, + /// sets up a `.then()` callback to re-invoke the current matcher once the + /// promise settles, stores the returned-to-caller promise in the + /// `resultValue` slot, and returns it. Returns `null` otherwise. + /// + /// This replaces the old synchronous `waitForPromise()` which would hang + /// forever if the promise could only be resolved by JavaScript sitting + /// above the matcher on the call stack (#14950). + fn maybeDeferMatcher(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, value: JSValue) bun.JSError!?JSValue { + if (this.flags.promise == .none) return null; + if (this.is_async_rerun) return null; + const promise = value.asAnyPromise() orelse return null; + if (promise.status() != .pending) return null; + + promise.setHandled(globalThis.vm()); + const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value); + js.resultValueSetCached(thisValue, globalThis, deferred); + return deferred; + } + + /// State for a `.resolves`/`.rejects` matcher call whose promise hasn't + /// settled yet. When it does, we re-invoke the matcher and resolve/reject + /// `deferred` with the outcome. + const PendingMatcher = struct { + /// The `Expect` JS instance (also keeps capturedValue alive). + expect_this: jsc.Strong.Optional, + /// The native matcher function being called (e.g. `toBe`). + matcher_fn: jsc.Strong.Optional, + /// JSArray of the arguments the matcher was called with. + matcher_args: jsc.Strong.Optional, + /// The promise returned to the caller of the matcher. + deferred: jsc.JSPromise.Strong, + + fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue) bun.JSError!JSValue { + const args = callframe.arguments(); + const args_array = try JSValue.createEmptyArray(globalThis, args.len); + for (args, 0..) |arg, i| { + try args_array.putIndex(globalThis, @intCast(i), arg); + } + + const pending = bun.new(PendingMatcher, .{ + .expect_this = .create(thisValue, globalThis), + .matcher_fn = .create(callframe.callee(), globalThis), + .matcher_args = .create(args_array, globalThis), + .deferred = jsc.JSPromise.Strong.init(globalThis), + }); + const deferred_value = pending.deferred.value(); + + promise_value.then(globalThis, pending, onResolve, onReject) catch { + // JSTerminated: the VM is shutting down. Clean up and return + // the (never-to-settle) promise; the caller will unwind. + pending.deinit(); + }; + + return deferred_value; + } + + pub export const Bun__Expect__PendingMatcher__onResolve = jsc.toJSHostFn(onResolve); + pub export const Bun__Expect__PendingMatcher__onReject = jsc.toJSHostFn(onReject); + + fn onResolve(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + _, const ctx = callframe.argumentsAsArray(2); + if (ctx.isEmptyOrUndefinedOrNull()) return .js_undefined; + const this: *PendingMatcher = ctx.asPromisePtr(PendingMatcher); + this.settle(globalThis); + return .js_undefined; + } + + fn onReject(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { + _, const ctx = callframe.argumentsAsArray(2); + if (ctx.isEmptyOrUndefinedOrNull()) return .js_undefined; + const this: *PendingMatcher = ctx.asPromisePtr(PendingMatcher); + this.settle(globalThis); + return .js_undefined; + } + + fn settle(this: *PendingMatcher, globalThis: *JSGlobalObject) void { + defer this.deinit(); + + const expect_this = this.expect_this.get() orelse return; + const matcher_fn = this.matcher_fn.get() orelse return; + const args_array = this.matcher_args.get() orelse return; + + // Extract the original arguments back out of the array. + const args_len: u32 = @intCast(@min( + args_array.getLength(globalThis) catch return, + max_matcher_args, + )); + var args_buf: [max_matcher_args]JSValue = @splat(.js_undefined); + for (0..args_len) |i| { + args_buf[i] = args_array.getIndex(globalThis, @intCast(i)) catch return; + } + + // Mark the re-run so we don't increment the expectation counter + // again or try to defer a second time. The captured promise is + // now settled, so `processPromise` will extract its result + // synchronously. + if (Expect.fromJS(expect_this)) |expect| expect.is_async_rerun = true; + defer if (Expect.fromJS(expect_this)) |expect| { + expect.is_async_rerun = false; + }; + + const result = matcher_fn.call(globalThis, expect_this, args_buf[0..args_len]) catch { + const exception = globalThis.tryTakeException() orelse JSValue.js_undefined; + this.deferred.reject(globalThis, exception) catch {}; + return; + }; + this.deferred.resolve(globalThis, result) catch {}; + } + + fn deinit(this: *PendingMatcher) void { + this.expect_this.deinit(); + this.matcher_fn.deinit(); + this.matcher_args.deinit(); + this.deferred.deinit(); + bun.destroy(this); + } + + /// Maximum number of positional arguments any matcher accepts. The + /// largest today is 2 (e.g. `toBeCloseTo(number, precision)`), but + /// custom matchers via `expect.extend()` can take more. + const max_matcher_args = 32; + }; /// Processes the async flags (resolves/rejects), waiting for the async value if needed. /// If no flags, returns the original value @@ -1155,6 +1301,9 @@ pub const Expect = struct { var value = js.capturedValueGetCached(thisValue) orelse { return globalThis.throw("Internal consistency error: failed to retrieve the captured value", .{}); }; + if (try expect.maybeDeferMatcher(globalThis, thisValue, callFrame, value)) |deferred| { + return deferred; + } value = try processPromise(expect.custom_label, expect.flags, globalThis, value, matcher_name, matcher_params, false); value.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toBe.zig b/src/runtime/test_runner/expect/toBe.zig index 0f8cba43179..202c689ec02 100644 --- a/src/runtime/test_runner/expect/toBe.zig +++ b/src/runtime/test_runner/expect/toBe.zig @@ -16,7 +16,7 @@ pub fn toBe( this.incrementExpectCallCounter(); const right = arguments[0]; right.ensureStillAlive(); - const left = try this.getValue(globalThis, thisValue, "toBe", "expected"); + const left = (try this.getValue(globalThis, thisValue, callframe, "toBe", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = try right.isSameValue(left, globalThis); diff --git a/src/runtime/test_runner/expect/toBeArray.zig b/src/runtime/test_runner/expect/toBeArray.zig index 7681a13e511..e1977e5a50b 100644 --- a/src/runtime/test_runner/expect/toBeArray.zig +++ b/src/runtime/test_runner/expect/toBeArray.zig @@ -2,7 +2,7 @@ pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeArray", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeArray", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeArrayOfSize.zig b/src/runtime/test_runner/expect/toBeArrayOfSize.zig index 00e26612759..f68de18da0a 100644 --- a/src/runtime/test_runner/expect/toBeArrayOfSize.zig +++ b/src/runtime/test_runner/expect/toBeArrayOfSize.zig @@ -9,7 +9,7 @@ pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeArrayOfSize", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeArrayOfSize", "")) orelse return this.deferredResult(thisValue); const size = arguments[0]; size.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toBeBoolean.zig b/src/runtime/test_runner/expect/toBeBoolean.zig index 1541a2e00dc..771aaf12121 100644 --- a/src/runtime/test_runner/expect/toBeBoolean.zig +++ b/src/runtime/test_runner/expect/toBeBoolean.zig @@ -2,7 +2,7 @@ pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeBoolean", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeBoolean", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeCloseTo.zig b/src/runtime/test_runner/expect/toBeCloseTo.zig index 83708f477b4..f62173e853a 100644 --- a/src/runtime/test_runner/expect/toBeCloseTo.zig +++ b/src/runtime/test_runner/expect/toBeCloseTo.zig @@ -26,7 +26,7 @@ pub fn toBeCloseTo(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF precision = precision_.asNumber(); } - const received_: JSValue = try this.getValue(globalThis, thisValue, "toBeCloseTo", "expected, precision"); + const received_: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeCloseTo", "expected, precision")) orelse return this.deferredResult(thisValue); if (!received_.isNumber()) { return globalThis.throwInvalidArgumentType("expect", "received", "number"); } diff --git a/src/runtime/test_runner/expect/toBeDate.zig b/src/runtime/test_runner/expect/toBeDate.zig index a1ee2a9cc40..83039cb4410 100644 --- a/src/runtime/test_runner/expect/toBeDate.zig +++ b/src/runtime/test_runner/expect/toBeDate.zig @@ -2,7 +2,7 @@ pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDate", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeDate", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeDefined.zig b/src/runtime/test_runner/expect/toBeDefined.zig index 0e3d5047086..06cb13d8945 100644 --- a/src/runtime/test_runner/expect/toBeDefined.zig +++ b/src/runtime/test_runner/expect/toBeDefined.zig @@ -2,7 +2,7 @@ pub fn toBeDefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDefined", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeDefined", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEmpty.zig b/src/runtime/test_runner/expect/toBeEmpty.zig index b3c33d52e63..a2dbeb1d643 100644 --- a/src/runtime/test_runner/expect/toBeEmpty.zig +++ b/src/runtime/test_runner/expect/toBeEmpty.zig @@ -2,7 +2,7 @@ pub fn toBeEmpty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmpty", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEmpty", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEmptyObject.zig b/src/runtime/test_runner/expect/toBeEmptyObject.zig index 4e9e09d044a..26870fbfa98 100644 --- a/src/runtime/test_runner/expect/toBeEmptyObject.zig +++ b/src/runtime/test_runner/expect/toBeEmptyObject.zig @@ -2,7 +2,7 @@ pub fn toBeEmptyObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmptyObject", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEmptyObject", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEven.zig b/src/runtime/test_runner/expect/toBeEven.zig index 55fdac875c0..189833cc048 100644 --- a/src/runtime/test_runner/expect/toBeEven.zig +++ b/src/runtime/test_runner/expect/toBeEven.zig @@ -3,7 +3,7 @@ pub fn toBeEven(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEven", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEven", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFalse.zig b/src/runtime/test_runner/expect/toBeFalse.zig index 3e5a83bb37e..f4c876bc6ec 100644 --- a/src/runtime/test_runner/expect/toBeFalse.zig +++ b/src/runtime/test_runner/expect/toBeFalse.zig @@ -2,7 +2,7 @@ pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalse", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFalse", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFalsy.zig b/src/runtime/test_runner/expect/toBeFalsy.zig index 7e66fe6a88f..eafd4a5deca 100644 --- a/src/runtime/test_runner/expect/toBeFalsy.zig +++ b/src/runtime/test_runner/expect/toBeFalsy.zig @@ -3,7 +3,7 @@ pub fn toBeFalsy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalsy", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFalsy", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFinite.zig b/src/runtime/test_runner/expect/toBeFinite.zig index 5e542764a98..08b44b78cdd 100644 --- a/src/runtime/test_runner/expect/toBeFinite.zig +++ b/src/runtime/test_runner/expect/toBeFinite.zig @@ -2,7 +2,7 @@ pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFinite", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFinite", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFunction.zig b/src/runtime/test_runner/expect/toBeFunction.zig index 5969c738540..8ffeb0fbd0b 100644 --- a/src/runtime/test_runner/expect/toBeFunction.zig +++ b/src/runtime/test_runner/expect/toBeFunction.zig @@ -2,7 +2,7 @@ pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFunction", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFunction", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeGreaterThan.zig b/src/runtime/test_runner/expect/toBeGreaterThan.zig index c19a1c81bb5..fdff2a0cb5a 100644 --- a/src/runtime/test_runner/expect/toBeGreaterThan.zig +++ b/src/runtime/test_runner/expect/toBeGreaterThan.zig @@ -14,7 +14,7 @@ pub fn toBeGreaterThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeGreaterThan", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeGreaterThan", "expected")) orelse return this.deferredResult(thisValue); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig b/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig index 7051638a138..fa4e81e2193 100644 --- a/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig +++ b/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig @@ -14,7 +14,7 @@ pub fn toBeGreaterThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFr const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeGreaterThanOrEqual", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeGreaterThanOrEqual", "expected")) orelse return this.deferredResult(thisValue); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeInstanceOf.zig b/src/runtime/test_runner/expect/toBeInstanceOf.zig index c0f5fcc87bd..0cf117b3e17 100644 --- a/src/runtime/test_runner/expect/toBeInstanceOf.zig +++ b/src/runtime/test_runner/expect/toBeInstanceOf.zig @@ -19,7 +19,7 @@ pub fn toBeInstanceOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca } expected_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeInstanceOf", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeInstanceOf", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = value.isInstanceOf(globalThis, expected_value); diff --git a/src/runtime/test_runner/expect/toBeInteger.zig b/src/runtime/test_runner/expect/toBeInteger.zig index 320f6991a37..57dfdbd7196 100644 --- a/src/runtime/test_runner/expect/toBeInteger.zig +++ b/src/runtime/test_runner/expect/toBeInteger.zig @@ -2,7 +2,7 @@ pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeInteger", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeInteger", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeLessThan.zig b/src/runtime/test_runner/expect/toBeLessThan.zig index 9ad24ba3605..f58e6994246 100644 --- a/src/runtime/test_runner/expect/toBeLessThan.zig +++ b/src/runtime/test_runner/expect/toBeLessThan.zig @@ -14,7 +14,7 @@ pub fn toBeLessThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeLessThan", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeLessThan", "expected")) orelse return this.deferredResult(thisValue); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig b/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig index 60cbe66f8c3..09198f45f4e 100644 --- a/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig +++ b/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig @@ -14,7 +14,7 @@ pub fn toBeLessThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeLessThanOrEqual", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeLessThanOrEqual", "expected")) orelse return this.deferredResult(thisValue); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeNaN.zig b/src/runtime/test_runner/expect/toBeNaN.zig index b8f49b7a39f..ce7c2dfde4f 100644 --- a/src/runtime/test_runner/expect/toBeNaN.zig +++ b/src/runtime/test_runner/expect/toBeNaN.zig @@ -2,7 +2,7 @@ pub fn toBeNaN(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNaN", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNaN", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNegative.zig b/src/runtime/test_runner/expect/toBeNegative.zig index ee6a0ca36d3..ff09705342f 100644 --- a/src/runtime/test_runner/expect/toBeNegative.zig +++ b/src/runtime/test_runner/expect/toBeNegative.zig @@ -2,7 +2,7 @@ pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNegative", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNegative", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNil.zig b/src/runtime/test_runner/expect/toBeNil.zig index 2b853154326..05f19fcd614 100644 --- a/src/runtime/test_runner/expect/toBeNil.zig +++ b/src/runtime/test_runner/expect/toBeNil.zig @@ -2,7 +2,7 @@ pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNil", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNil", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNull.zig b/src/runtime/test_runner/expect/toBeNull.zig index 1a1f05d35cb..e2882b1ff86 100644 --- a/src/runtime/test_runner/expect/toBeNull.zig +++ b/src/runtime/test_runner/expect/toBeNull.zig @@ -2,7 +2,7 @@ pub fn toBeNull(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNull", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNull", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNumber.zig b/src/runtime/test_runner/expect/toBeNumber.zig index cb94f40860a..6283d35bb8f 100644 --- a/src/runtime/test_runner/expect/toBeNumber.zig +++ b/src/runtime/test_runner/expect/toBeNumber.zig @@ -2,7 +2,7 @@ pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNumber", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNumber", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeObject.zig b/src/runtime/test_runner/expect/toBeObject.zig index 474b946db90..12b0d94fbe5 100644 --- a/src/runtime/test_runner/expect/toBeObject.zig +++ b/src/runtime/test_runner/expect/toBeObject.zig @@ -2,7 +2,7 @@ pub fn toBeObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeObject", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeObject", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeOdd.zig b/src/runtime/test_runner/expect/toBeOdd.zig index a0cb5b0a75f..f826d563838 100644 --- a/src/runtime/test_runner/expect/toBeOdd.zig +++ b/src/runtime/test_runner/expect/toBeOdd.zig @@ -3,7 +3,7 @@ pub fn toBeOdd(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeOdd", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeOdd", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeOneOf.zig b/src/runtime/test_runner/expect/toBeOneOf.zig index 17b05f4bc84..6eb9a500305 100644 --- a/src/runtime/test_runner/expect/toBeOneOf.zig +++ b/src/runtime/test_runner/expect/toBeOneOf.zig @@ -14,7 +14,7 @@ pub fn toBeOneOf( this.incrementExpectCallCounter(); - const expected = try this.getValue(globalThis, thisValue, "toBeOneOf", "expected"); + const expected = (try this.getValue(globalThis, thisValue, callFrame, "toBeOneOf", "expected")) orelse return this.deferredResult(thisValue); const list_value: JSValue = arguments[0]; const not = this.flags.not; diff --git a/src/runtime/test_runner/expect/toBePositive.zig b/src/runtime/test_runner/expect/toBePositive.zig index 620090800d1..6384fc77bad 100644 --- a/src/runtime/test_runner/expect/toBePositive.zig +++ b/src/runtime/test_runner/expect/toBePositive.zig @@ -2,7 +2,7 @@ pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBePositive", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBePositive", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeString.zig b/src/runtime/test_runner/expect/toBeString.zig index def65673efd..f75f1026a46 100644 --- a/src/runtime/test_runner/expect/toBeString.zig +++ b/src/runtime/test_runner/expect/toBeString.zig @@ -2,7 +2,7 @@ pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeString", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeString", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeSymbol.zig b/src/runtime/test_runner/expect/toBeSymbol.zig index be06ed94090..fbd9b1f9c26 100644 --- a/src/runtime/test_runner/expect/toBeSymbol.zig +++ b/src/runtime/test_runner/expect/toBeSymbol.zig @@ -2,7 +2,7 @@ pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeSymbol", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeSymbol", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTrue.zig b/src/runtime/test_runner/expect/toBeTrue.zig index 1764550df10..e101e524840 100644 --- a/src/runtime/test_runner/expect/toBeTrue.zig +++ b/src/runtime/test_runner/expect/toBeTrue.zig @@ -2,7 +2,7 @@ pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTrue", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTrue", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTruthy.zig b/src/runtime/test_runner/expect/toBeTruthy.zig index e6043e60ef6..7ac5363a7ec 100644 --- a/src/runtime/test_runner/expect/toBeTruthy.zig +++ b/src/runtime/test_runner/expect/toBeTruthy.zig @@ -1,7 +1,7 @@ pub fn toBeTruthy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTruthy", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTruthy", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTypeOf.zig b/src/runtime/test_runner/expect/toBeTypeOf.zig index ccc7773d4ed..19ff4808754 100644 --- a/src/runtime/test_runner/expect/toBeTypeOf.zig +++ b/src/runtime/test_runner/expect/toBeTypeOf.zig @@ -20,7 +20,7 @@ pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr return globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTypeOf", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTypeOf", "")) orelse return this.deferredResult(thisValue); const expected = arguments[0]; expected.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toBeUndefined.zig b/src/runtime/test_runner/expect/toBeUndefined.zig index 2230519f5a6..cf8f405d718 100644 --- a/src/runtime/test_runner/expect/toBeUndefined.zig +++ b/src/runtime/test_runner/expect/toBeUndefined.zig @@ -1,7 +1,7 @@ pub fn toBeUndefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeUndefined", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeUndefined", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeValidDate.zig b/src/runtime/test_runner/expect/toBeValidDate.zig index b274faec228..d323c445dad 100644 --- a/src/runtime/test_runner/expect/toBeValidDate.zig +++ b/src/runtime/test_runner/expect/toBeValidDate.zig @@ -2,7 +2,7 @@ pub fn toBeValidDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeValidDate", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeValidDate", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeWithin.zig b/src/runtime/test_runner/expect/toBeWithin.zig index 772aef3cd55..1e6dc6bd622 100644 --- a/src/runtime/test_runner/expect/toBeWithin.zig +++ b/src/runtime/test_runner/expect/toBeWithin.zig @@ -9,7 +9,7 @@ pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr return globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toBeWithin", "start, end"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeWithin", "start, end")) orelse return this.deferredResult(thisValue); const startValue = arguments[0]; startValue.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toContain.zig b/src/runtime/test_runner/expect/toContain.zig index 45b6d44c8f2..dd32530b8e1 100644 --- a/src/runtime/test_runner/expect/toContain.zig +++ b/src/runtime/test_runner/expect/toContain.zig @@ -16,7 +16,7 @@ pub fn toContain( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toContain", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContain", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainAllKeys.zig b/src/runtime/test_runner/expect/toContainAllKeys.zig index f494e5ea436..ea656d4f1b8 100644 --- a/src/runtime/test_runner/expect/toContainAllKeys.zig +++ b/src/runtime/test_runner/expect/toContainAllKeys.zig @@ -16,7 +16,7 @@ pub fn toContainAllKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAllKeys", "expected"); + const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAllKeys", "expected")) orelse return this.deferredResult(thisValue); if (!expected.jsType().isArray()) { return globalObject.throwInvalidArgumentType("toContainAllKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainAllValues.zig b/src/runtime/test_runner/expect/toContainAllValues.zig index 3f8cb53993a..dd0fc1c9ea4 100644 --- a/src/runtime/test_runner/expect/toContainAllValues.zig +++ b/src/runtime/test_runner/expect/toContainAllValues.zig @@ -19,7 +19,7 @@ pub fn toContainAllValues( return globalObject.throwInvalidArgumentType("toContainAllValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAllValues", "expected"); + const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAllValues", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainAnyKeys.zig b/src/runtime/test_runner/expect/toContainAnyKeys.zig index aaebf75f9ae..b5ecbb0ffba 100644 --- a/src/runtime/test_runner/expect/toContainAnyKeys.zig +++ b/src/runtime/test_runner/expect/toContainAnyKeys.zig @@ -16,7 +16,7 @@ pub fn toContainAnyKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toContainAnyKeys", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainAnyKeys", "expected")) orelse return this.deferredResult(thisValue); if (!expected.jsType().isArray()) { return globalThis.throwInvalidArgumentType("toContainAnyKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainAnyValues.zig b/src/runtime/test_runner/expect/toContainAnyValues.zig index ea3dc18c6a7..022cc21c2ab 100644 --- a/src/runtime/test_runner/expect/toContainAnyValues.zig +++ b/src/runtime/test_runner/expect/toContainAnyValues.zig @@ -19,7 +19,7 @@ pub fn toContainAnyValues( return globalObject.throwInvalidArgumentType("toContainAnyValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAnyValues", "expected"); + const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAnyValues", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainEqual.zig b/src/runtime/test_runner/expect/toContainEqual.zig index b4b4e69cda6..834b7f8045f 100644 --- a/src/runtime/test_runner/expect/toContainEqual.zig +++ b/src/runtime/test_runner/expect/toContainEqual.zig @@ -16,7 +16,7 @@ pub fn toContainEqual( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toContainEqual", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainEqual", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainKey.zig b/src/runtime/test_runner/expect/toContainKey.zig index 0121e85c8a3..9dcebf66b9f 100644 --- a/src/runtime/test_runner/expect/toContainKey.zig +++ b/src/runtime/test_runner/expect/toContainKey.zig @@ -16,7 +16,7 @@ pub fn toContainKey( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toContainKey", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainKey", "expected")) orelse return this.deferredResult(thisValue); var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); diff --git a/src/runtime/test_runner/expect/toContainKeys.zig b/src/runtime/test_runner/expect/toContainKeys.zig index eef143f81cb..d3099456d77 100644 --- a/src/runtime/test_runner/expect/toContainKeys.zig +++ b/src/runtime/test_runner/expect/toContainKeys.zig @@ -16,7 +16,7 @@ pub fn toContainKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toContainKeys", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainKeys", "expected")) orelse return this.deferredResult(thisValue); if (!expected.jsType().isArray()) { return globalThis.throwInvalidArgumentType("toContainKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainValue.zig b/src/runtime/test_runner/expect/toContainValue.zig index 31f92c96bed..dfdc7550cfa 100644 --- a/src/runtime/test_runner/expect/toContainValue.zig +++ b/src/runtime/test_runner/expect/toContainValue.zig @@ -16,7 +16,7 @@ pub fn toContainValue( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalObject, thisValue, "toContainValue", "expected"); + const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainValue", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainValues.zig b/src/runtime/test_runner/expect/toContainValues.zig index 567bd7e1b80..751b5fa24b9 100644 --- a/src/runtime/test_runner/expect/toContainValues.zig +++ b/src/runtime/test_runner/expect/toContainValues.zig @@ -19,7 +19,7 @@ pub fn toContainValues( return globalObject.throwInvalidArgumentType("toContainValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = try this.getValue(globalObject, thisValue, "toContainValues", "expected"); + const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainValues", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = true; diff --git a/src/runtime/test_runner/expect/toEndWith.zig b/src/runtime/test_runner/expect/toEndWith.zig index 0bdea7e46da..d3b65536b4a 100644 --- a/src/runtime/test_runner/expect/toEndWith.zig +++ b/src/runtime/test_runner/expect/toEndWith.zig @@ -16,7 +16,7 @@ pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra return globalThis.throw("toEndWith() requires the first argument to be a string", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toEndWith", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEndWith", "expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toEqual.zig b/src/runtime/test_runner/expect/toEqual.zig index 614b9fc549a..6ce89298984 100644 --- a/src/runtime/test_runner/expect/toEqual.zig +++ b/src/runtime/test_runner/expect/toEqual.zig @@ -12,7 +12,7 @@ pub fn toEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = try this.getValue(globalThis, thisValue, "toEqual", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEqual", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = try value.jestDeepEquals(expected, globalThis); diff --git a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig index 18acdaeb078..e14cb18ba4f 100644 --- a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig +++ b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig @@ -12,7 +12,7 @@ pub fn toEqualIgnoringWhitespace(this: *Expect, globalThis: *JSGlobalObject, cal this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = try this.getValue(globalThis, thisValue, "toEqualIgnoringWhitespace", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEqualIgnoringWhitespace", "expected")) orelse return this.deferredResult(thisValue); if (!expected.isString()) { return globalThis.throw("toEqualIgnoringWhitespace() requires argument to be a string", .{}); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalled.zig b/src/runtime/test_runner/expect/toHaveBeenCalled.zig index a4113d1f045..f13160b1c5c 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalled.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalled.zig @@ -8,7 +8,7 @@ pub fn toHaveBeenCalled(this: *Expect, globalThis: *JSGlobalObject, callframe: * return globalThis.throwInvalidArguments("toHaveBeenCalled() must not have an argument", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalled", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalled", "")) orelse return this.deferredResult(thisValue); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig index dbb20dac0ce..18d206e7d6e 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig @@ -3,7 +3,7 @@ pub fn toHaveBeenCalledOnce(this: *Expect, globalThis: *JSGlobalObject, callfram const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledOnce", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledOnce", "expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig index 5b0db03abaa..62427dae5a2 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig @@ -5,7 +5,7 @@ pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callfra const arguments_ = callframe.arguments_old(1); const arguments: []const JSValue = arguments_.slice(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledTimes", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledTimes", "expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig index d49c572394b..f17d6f15070 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenCalledWith(this: *Expect, globalThis: *JSGlobalObject, callfram const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledWith", "...expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledWith", "...expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig index 488a42916e0..4da2e02a5a4 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenLastCalledWith(this: *Expect, globalThis: *JSGlobalObject, call const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastCalledWith", "...expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenLastCalledWith", "...expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig index 9ebcd9cb280..ccf30eaaf13 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenNthCalledWith(this: *Expect, globalThis: *JSGlobalObject, callf const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenNthCalledWith", "n, ...expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenNthCalledWith", "n, ...expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig b/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig index 82b7230a61a..6608f5c4d75 100644 --- a/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig @@ -4,7 +4,7 @@ pub fn toHaveLastReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfr const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastReturnedWith", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenLastReturnedWith", "expected")) orelse return this.deferredResult(thisValue); const expected = callframe.argumentsAsArray(1)[0]; this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveLength.zig b/src/runtime/test_runner/expect/toHaveLength.zig index 5aa7a349ef9..fcb1b107b4a 100644 --- a/src/runtime/test_runner/expect/toHaveLength.zig +++ b/src/runtime/test_runner/expect/toHaveLength.zig @@ -15,7 +15,7 @@ pub fn toHaveLength( this.incrementExpectCallCounter(); const expected: JSValue = arguments[0]; - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveLength", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveLength", "expected")) orelse return this.deferredResult(thisValue); if (!value.isObject() and !value.isString()) { var fmt = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; diff --git a/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig b/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig index 5ba76d46a07..5c0cfae6ea2 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig @@ -2,7 +2,7 @@ pub fn toHaveNthReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfra jsc.markBinding(@src()); const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveNthReturnedWith", "n, expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveNthReturnedWith", "n, expected")) orelse return this.deferredResult(thisValue); const nth_arg, const expected = callframe.argumentsAsArray(2); diff --git a/src/runtime/test_runner/expect/toHaveProperty.zig b/src/runtime/test_runner/expect/toHaveProperty.zig index 019c815511e..ce326f35c4d 100644 --- a/src/runtime/test_runner/expect/toHaveProperty.zig +++ b/src/runtime/test_runner/expect/toHaveProperty.zig @@ -16,7 +16,7 @@ pub fn toHaveProperty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; if (expected_property) |ev| ev.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveProperty", "path, value"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toHaveProperty", "path, value")) orelse return this.deferredResult(thisValue); if (!expected_property_path.isString() and !try expected_property_path.isIterable(globalThis)) { return globalThis.throw("Expected path must be a string or an array", .{}); diff --git a/src/runtime/test_runner/expect/toHaveReturned.zig b/src/runtime/test_runner/expect/toHaveReturned.zig index 75438185fcd..3c842dcb971 100644 --- a/src/runtime/test_runner/expect/toHaveReturned.zig +++ b/src/runtime/test_runner/expect/toHaveReturned.zig @@ -5,7 +5,7 @@ inline fn toHaveReturnedTimesFn(this: *Expect, globalThis: *JSGlobalObject, call const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, @tagName(mode), "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, @tagName(mode), "expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveReturnedWith.zig b/src/runtime/test_runner/expect/toHaveReturnedWith.zig index c8aac164a5b..0a20538506a 100644 --- a/src/runtime/test_runner/expect/toHaveReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveReturnedWith.zig @@ -4,7 +4,7 @@ pub fn toHaveReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callframe: const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveReturnedWith", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveReturnedWith", "expected")) orelse return this.deferredResult(thisValue); const expected = callframe.argumentsAsArray(1)[0]; this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toInclude.zig b/src/runtime/test_runner/expect/toInclude.zig index fe8711f0d60..204a9834abf 100644 --- a/src/runtime/test_runner/expect/toInclude.zig +++ b/src/runtime/test_runner/expect/toInclude.zig @@ -16,7 +16,7 @@ pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra return globalThis.throw("toInclude() requires the first argument to be a string", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toInclude", ""); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toInclude", "")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toMatch.zig b/src/runtime/test_runner/expect/toMatch.zig index 3b8d877dccc..67501f41af8 100644 --- a/src/runtime/test_runner/expect/toMatch.zig +++ b/src/runtime/test_runner/expect/toMatch.zig @@ -22,7 +22,7 @@ pub fn toMatch(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame } expected_value.ensureStillAlive(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toMatch", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatch", "expected")) orelse return this.deferredResult(thisValue); if (!value.isString()) { return globalThis.throw("Received value must be a string: {f}", .{value.toFmt(&formatter)}); diff --git a/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig b/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig index 92faf91ffec..44e9c73efdb 100644 --- a/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig +++ b/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig @@ -47,7 +47,7 @@ pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFra const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; - const value = try this.getValue(globalThis, thisValue, "toMatchInlineSnapshot", "properties, hint"); + const value = (try this.getValue(globalThis, thisValue, callFrame, "toMatchInlineSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); return this.inlineSnapshot(globalThis, callFrame, value, property_matchers, expected_slice, "toMatchInlineSnapshot"); } diff --git a/src/runtime/test_runner/expect/toMatchObject.zig b/src/runtime/test_runner/expect/toMatchObject.zig index e9b065e05ae..e8e3557141e 100644 --- a/src/runtime/test_runner/expect/toMatchObject.zig +++ b/src/runtime/test_runner/expect/toMatchObject.zig @@ -9,7 +9,7 @@ pub fn toMatchObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal const not = this.flags.not; - const received_object: JSValue = try this.getValue(globalThis, thisValue, "toMatchObject", "expected"); + const received_object: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatchObject", "expected")) orelse return this.deferredResult(thisValue); if (!received_object.isObject()) { const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; diff --git a/src/runtime/test_runner/expect/toMatchSnapshot.zig b/src/runtime/test_runner/expect/toMatchSnapshot.zig index 30de93411dc..f69a989a509 100644 --- a/src/runtime/test_runner/expect/toMatchSnapshot.zig +++ b/src/runtime/test_runner/expect/toMatchSnapshot.zig @@ -50,7 +50,7 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C var hint = hint_string.toSlice(default_allocator); defer hint.deinit(); - const value: JSValue = try this.getValue(globalThis, thisValue, "toMatchSnapshot", "properties, hint"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatchSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); return this.snapshot(globalThis, value, property_matchers, hint.slice(), "toMatchSnapshot"); } diff --git a/src/runtime/test_runner/expect/toStartWith.zig b/src/runtime/test_runner/expect/toStartWith.zig index ddf71443417..618d605c518 100644 --- a/src/runtime/test_runner/expect/toStartWith.zig +++ b/src/runtime/test_runner/expect/toStartWith.zig @@ -16,7 +16,7 @@ pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF return globalThis.throw("toStartWith() requires the first argument to be a string", .{}); } - const value: JSValue = try this.getValue(globalThis, thisValue, "toStartWith", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toStartWith", "expected")) orelse return this.deferredResult(thisValue); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toStrictEqual.zig b/src/runtime/test_runner/expect/toStrictEqual.zig index 47414be4775..1b5ca42651c 100644 --- a/src/runtime/test_runner/expect/toStrictEqual.zig +++ b/src/runtime/test_runner/expect/toStrictEqual.zig @@ -12,7 +12,7 @@ pub fn toStrictEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = try this.getValue(globalThis, thisValue, "toStrictEqual", "expected"); + const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toStrictEqual", "expected")) orelse return this.deferredResult(thisValue); const not = this.flags.not; var pass = try value.jestStrictDeepEquals(expected, globalThis); diff --git a/src/runtime/test_runner/expect/toThrow.zig b/src/runtime/test_runner/expect/toThrow.zig index 9927bea0bda..a8847977bcf 100644 --- a/src/runtime/test_runner/expect/toThrow.zig +++ b/src/runtime/test_runner/expect/toThrow.zig @@ -32,7 +32,8 @@ pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const not = this.flags.not; - const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, try this.getValue(globalThis, thisValue, "toThrow", "expected")); + const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrow", "expected")) orelse return this.deferredResult(thisValue); + const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, received); const did_throw = result_ != null; diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig index 723a936c61d..c93d2d5b801 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig @@ -32,7 +32,8 @@ pub fn toThrowErrorMatchingInlineSnapshot(this: *Expect, globalThis: *JSGlobalOb const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; - const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingInlineSnapshot", "properties, hint"))) orelse { + const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrowErrorMatchingInlineSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); + const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, received)) orelse { const signature = comptime getSignature("toThrowErrorMatchingInlineSnapshot", "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Received function did not throw\n", .{}); }; diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig index 5162a14557e..334dad51d7c 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig @@ -34,7 +34,8 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, var hint = hint_string.toSlice(default_allocator); defer hint.deinit(); - const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingSnapshot", "properties, hint"))) orelse { + const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrowErrorMatchingSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); + const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, received)) orelse { const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Received function did not throw\n", .{}); }; diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts new file mode 100644 index 00000000000..5d68e2b39b0 --- /dev/null +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -0,0 +1,198 @@ +// https://github.com/oven-sh/bun/issues/14950 +// +// `expect(promise).resolves.()` used to synchronously spin the +// event loop (`waitForPromise`) until the promise settled. If the only +// thing that could settle it was JS still sitting above the matcher on +// the call stack, the test hung at 100% CPU — not even the test-level +// timeout could interrupt it. +// +// All of the hang-prone cases are exercised in a subprocess so that this +// file fails (rather than hangs) on a build without the fix. + +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +async function runFixture(name: string, source: string) { + using dir = tempDir(`expect-resolves-${name}`, { "sub.test.js": source }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "sub.test.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + // On a build without the fix the subprocess hangs, so bound the wait. + const timedOut = await Promise.race([proc.exited.then(() => false), Bun.sleep(20_000).then(() => true)]); + if (timedOut) { + proc.kill("SIGKILL"); + await proc.exited; + } + + const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]); + return { out: stdout + stderr, exitCode: proc.exitCode, timedOut }; +} + +describe("expect().resolves / .rejects on a still-pending promise", () => { + // Exact reproduction from #14950, plus the .rejects mirror and several + // matcher variants. + test("does not hang when the promise is settled after the matcher call", async () => { + const { out, exitCode, timedOut } = await runFixture( + "pass", + /* js */ ` + import { test, expect } from "bun:test"; + + test("resolves.toBe, resolved after matcher (no await)", () => { + let resolve; + expect(new Promise(r => (resolve = r))).resolves.toBe(25); + resolve(25); + }); + + test("rejects.toBe, rejected after matcher (no await)", () => { + let reject; + expect(new Promise((_, r) => (reject = r))).rejects.toBe("err"); + reject("err"); + }); + + test("resolves.toBe awaited", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBe(42); + if (!(assertion instanceof Promise)) { + throw new Error("expected .resolves matcher to return a Promise for a pending input"); + } + resolve(42); + await assertion; + }); + + test("rejects.toThrow awaited", async () => { + let reject; + const assertion = expect(new Promise((_, r) => (reject = r))).rejects.toThrow("boom"); + reject(new Error("boom")); + await assertion; + }); + + test("resolves.not.toBe awaited", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.not.toBe(100); + resolve(99); + await assertion; + }); + + test("resolves.toEqual awaited", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toEqual({ a: 1 }); + resolve({ a: 1 }); + await assertion; + }); + + test("resolves resolved in a later task", async () => { + let resolve; + const prom = new Promise(r => (resolve = r)); + setImmediate(() => resolve(7)); + await expect(prom).resolves.toBe(7); + }); + + expect.extend({ + toBeFoo(received) { + return { + pass: received === "foo", + message: () => "expected " + received + ' to be "foo"', + }; + }, + }); + + test("custom matcher via expect.extend", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBeFoo(); + resolve("foo"); + await assertion; + }); + `, + ); + + expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); + expect(out).toContain("8 pass"); + expect(out).toContain("0 fail"); + expect(out).not.toContain("timed out"); + }, 40_000); + + test("a failing deferred assertion still fails the test", async () => { + const { out, exitCode, timedOut } = await runFixture( + "fail", + /* js */ ` + import { test, expect } from "bun:test"; + + test("resolves.toBe mismatch (awaited)", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBe(25); + resolve(99); + await assertion; + }); + + test("rejects on a promise that resolves (awaited)", async () => { + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).rejects.toBe(25); + resolve(25); + await assertion; + }); + + test("resolves.toBe mismatch (not awaited)", () => { + let resolve; + expect(new Promise(r => (resolve = r))).resolves.toBe(25); + resolve(99); + }); + `, + ); + + expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 1 }); + expect(out).toContain("0 pass"); + expect(out).toContain("3 fail"); + expect(out).toContain("Expected: 25"); + expect(out).toContain("Received: 99"); + expect(out).toContain("Expected promise that rejects"); + expect(out).not.toContain("timed out"); + }, 40_000); + + test("expect.assertions counts a deferred matcher exactly once", async () => { + const { out, exitCode, timedOut } = await runFixture( + "assertions", + /* js */ ` + import { test, expect } from "bun:test"; + + test("one deferred assertion", async () => { + expect.assertions(1); + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBe(5); + resolve(5); + await assertion; + }); + `, + ); + + expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); + expect(out).toContain("1 pass"); + expect(out).toContain("0 fail"); + }, 40_000); +}); + +// Already-settled promises took the synchronous path before and after the +// fix, so these are safe to run inline. +describe("expect().resolves / .rejects on an already-settled promise", () => { + test("resolves.toBe", async () => { + await expect(Promise.resolve(99)).resolves.toBe(99); + }); + + test("rejects.toBe", async () => { + await expect(Promise.reject(99)).rejects.toBe(99); + }); + + test("resolves on a rejected promise throws", async () => { + let caught: unknown; + try { + await expect(Promise.reject(1)).resolves.toBe(1); + } catch (e) { + caught = e; + } + expect(String(caught)).toContain("Expected promise that resolves"); + }); +}); From e205cc21317a773ef85a97dfba2ac23f0fa5042f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 00:34:46 +0000 Subject: [PATCH 02/26] Add test.concurrent coverage for #25181 --- .../bun/test/expect-resolves-pending.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 5d68e2b39b0..825e26a41bd 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -173,6 +173,44 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { expect(out).toContain("1 pass"); expect(out).toContain("0 fail"); }, 40_000); + + // https://github.com/oven-sh/bun/issues/25181 + // With the blocking `waitForPromise()`, each concurrent test's + // `.resolves` serialized the whole group. Ten 1s-sleeps took ~10s; + // now they overlap. + test("does not serialize test.concurrent tests", async () => { + const { out, exitCode, timedOut } = await runFixture( + "concurrent", + /* js */ ` + import { test, expect, afterAll } from "bun:test"; + + const start = Date.now(); + + async function slow() { + await new Promise(r => setTimeout(r, 1000)); + return { ok: true }; + } + + test.concurrent.each([...Array(10).keys()])("concurrent %i", async () => { + await expect(slow()).resolves.toEqual({ ok: true }); + }); + + afterAll(() => { + console.log("ELAPSED=" + (Date.now() - start)); + }); + `, + ); + + expect(timedOut).toBe(false); + expect(out).toContain("10 pass"); + expect(out).toContain("0 fail"); + const elapsed = Number(out.match(/ELAPSED=(\d+)/)?.[1]); + // Ten 1s-sleeps: concurrent ≈ 1s, serialized ≈ 10s. Allow generous + // slack for slow CI — anything under 5s proves they overlapped. + expect(elapsed).toBeGreaterThan(900); + expect(elapsed).toBeLessThan(5000); + expect(exitCode).toBe(0); + }, 40_000); }); // Already-settled promises took the synchronous path before and after the From c1d31cb33c987cc806076505fc8e911dd61cf1e6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 00:57:46 +0000 Subject: [PATCH 03/26] Address review: srcloc capture for inline snapshots, correct expect() counting, unbounded matcher args - Capture the caller's source location in maybeDeferMatcher() and use it in inlineSnapshot() on the deferred re-run, since the user's frame is no longer on the stack. Fixes regression for await expect(asyncFn()).resolves.toMatchInlineSnapshot(). - Replace the is_async_rerun guard in incrementExpectCallCounter() with a counted_expect_call flag on the Expect instance. Matchers call the counter both before and after getValue(); the old guard under-counted the ~40 matchers that increment after. - Use a stackFallback allocator for the re-run's argument list instead of a hard 32-element cap, since toHaveBeenCalledWith and custom matchers are variadic. Added test coverage for all three. --- src/runtime/test_runner/expect.zig | 65 +++++++++++++------ .../bun/test/expect-resolves-pending.test.ts | 55 +++++++++++++++- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index d3d862e5ec8..b8cde58ff7f 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -25,9 +25,17 @@ pub const Expect = struct { parent: ?*bun.jsc.Jest.bun_test.BunTest.RefData, custom_label: bun.String = bun.String.empty, /// Set to true while re-invoking a matcher from a `.resolves`/`.rejects` - /// `.then()` callback so we don't double-count the expectation or defer - /// again. + /// `.then()` callback so we don't try to defer again. is_async_rerun: bool = false, + /// Set by `incrementExpectCallCounter()` on the first call for this + /// `expect()` instance so a deferred re-run doesn't count the same + /// expectation twice. Matchers call `incrementExpectCallCounter()` + /// either before or after `getValue()`, so we can't rely on ordering. + counted_expect_call: bool = false, + /// The caller's source location, captured when a `.resolves`/`.rejects` + /// matcher is deferred. `inlineSnapshot()` needs this on the re-run + /// because the user's frame is no longer on the stack. + async_rerun_srcloc: ?CallFrame.CallerSrcLoc = null, pub const TestScope = struct { test_id: TestRunner.Test.ID, @@ -35,7 +43,8 @@ pub const Expect = struct { }; pub fn incrementExpectCallCounter(this: *Expect) void { - if (this.is_async_rerun) return; // already counted on the first (deferred) call + if (this.counted_expect_call) return; + this.counted_expect_call = true; const parent = this.parent orelse return; // not in bun:test var buntest_strong = parent.bunTest() orelse return; // the test file this expect() call was for is no longer defer buntest_strong.deinit(); @@ -207,6 +216,14 @@ pub const Expect = struct { if (promise.status() != .pending) return null; promise.setHandled(globalThis.vm()); + + // Capture the caller's source location now, while the user's frame + // is still on the stack. `toMatchInlineSnapshot` / + // `toThrowErrorMatchingInlineSnapshot` need it on the re-run to + // know which line to write the snapshot back to. + if (this.async_rerun_srcloc) |*old| old.str.deref(); + this.async_rerun_srcloc = callframe.getCallerSrcLoc(globalThis); + const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value); js.resultValueSetCached(thisValue, globalThis, deferred); return deferred; @@ -276,25 +293,27 @@ pub const Expect = struct { const args_array = this.matcher_args.get() orelse return; // Extract the original arguments back out of the array. - const args_len: u32 = @intCast(@min( - args_array.getLength(globalThis) catch return, - max_matcher_args, - )); - var args_buf: [max_matcher_args]JSValue = @splat(.js_undefined); + // Most built-in matchers take 0-2 arguments, but + // `toHaveBeenCalledWith` and `expect.extend()` custom matchers + // are variadic, so fall back to the heap when needed. + const args_len: u32 = @intCast(args_array.getLength(globalThis) catch return); + var sfa = std.heap.stackFallback(inline_matcher_args * @sizeOf(JSValue), bun.default_allocator); + const allocator = sfa.get(); + const args_buf = allocator.alloc(JSValue, args_len) catch bun.outOfMemory(); + defer allocator.free(args_buf); for (0..args_len) |i| { args_buf[i] = args_array.getIndex(globalThis, @intCast(i)) catch return; } - // Mark the re-run so we don't increment the expectation counter - // again or try to defer a second time. The captured promise is - // now settled, so `processPromise` will extract its result - // synchronously. + // Mark the re-run so we don't try to defer a second time. The + // captured promise is now settled, so `processPromise` will + // extract its result synchronously. if (Expect.fromJS(expect_this)) |expect| expect.is_async_rerun = true; defer if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = false; }; - const result = matcher_fn.call(globalThis, expect_this, args_buf[0..args_len]) catch { + const result = matcher_fn.call(globalThis, expect_this, args_buf) catch { const exception = globalThis.tryTakeException() orelse JSValue.js_undefined; this.deferred.reject(globalThis, exception) catch {}; return; @@ -310,10 +329,11 @@ pub const Expect = struct { bun.destroy(this); } - /// Maximum number of positional arguments any matcher accepts. The - /// largest today is 2 (e.g. `toBeCloseTo(number, precision)`), but - /// custom matchers via `expect.extend()` can take more. - const max_matcher_args = 32; + /// Stack-buffer size for the re-run's argument list. Most built-in + /// matchers take 0-2 positional arguments; `toHaveBeenCalledWith` + /// and custom matchers via `expect.extend()` are variadic, so + /// `settle()` heap-allocates past this. + const inline_matcher_args = 8; }; /// Processes the async flags (resolves/rejects), waiting for the async value if needed. @@ -475,6 +495,7 @@ pub const Expect = struct { ) callconv(.c) void { this.custom_label.deref(); if (this.parent) |parent| parent.deref(); + if (this.async_rerun_srcloc) |*s| s.str.deref(); VirtualMachine.get().allocator.destroy(this); } @@ -905,8 +926,14 @@ pub const Expect = struct { const buntest = buntest_strong.get(); // 1. find the src loc of the snapshot - const srcloc = callFrame.getCallerSrcLoc(globalThis); - defer srcloc.str.deref(); + // When re-running from a deferred `.resolves`/`.rejects` + // `.then()` callback, the user's frame is no longer on the + // stack; use the location captured in `maybeDeferMatcher()`. + const srcloc: CallFrame.CallerSrcLoc, const owns_srcloc: bool = if (this.async_rerun_srcloc) |s| + .{ s, false } + else + .{ callFrame.getCallerSrcLoc(globalThis), true }; + defer if (owns_srcloc) srcloc.str.deref(); const file_id = buntest.file_id; const fget = runner.files.get(file_id); diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 825e26a41bd..e6c1694616b 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -12,11 +12,11 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; -async function runFixture(name: string, source: string) { +async function runFixture(name: string, source: string, extraEnv: Record = {}) { using dir = tempDir(`expect-resolves-${name}`, { "sub.test.js": source }); await using proc = Bun.spawn({ cmd: [bunExe(), "test", "sub.test.js"], - env: bunEnv, + env: { ...bunEnv, ...extraEnv }, cwd: String(dir), stdout: "pipe", stderr: "pipe", @@ -153,25 +153,74 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { expect(out).not.toContain("timed out"); }, 40_000); + // Matchers are inconsistent about whether they call + // `incrementExpectCallCounter()` before or after `getValue()`. Both + // orderings must count exactly once through the deferred path. test("expect.assertions counts a deferred matcher exactly once", async () => { const { out, exitCode, timedOut } = await runFixture( "assertions", /* js */ ` import { test, expect } from "bun:test"; - test("one deferred assertion", async () => { + expect.extend({ + toBeBar(received) { + return { pass: received === "bar", message: () => "expected bar" }; + }, + }); + + test("toBe: increments before getValue", async () => { expect.assertions(1); let resolve; const assertion = expect(new Promise(r => (resolve = r))).resolves.toBe(5); resolve(5); await assertion; }); + + test("toBeTruthy: increments after getValue", async () => { + expect.assertions(1); + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBeTruthy(); + resolve("x"); + await assertion; + }); + + test("custom matcher: increments after maybeDeferMatcher", async () => { + expect.assertions(1); + let resolve; + const assertion = expect(new Promise(r => (resolve = r))).resolves.toBeBar(); + resolve("bar"); + await assertion; + }); + `, + ); + + expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); + expect(out).toContain("3 pass"); + expect(out).toContain("0 fail"); + }, 40_000); + + // On the deferred re-run the user's frame is gone from the stack, so + // `inlineSnapshot()` has to use the source location captured on the + // first call. Only the write path reads the source location, so this + // exercises it by creating a new snapshot (hence `CI: "false"`). + test("toMatchInlineSnapshot writes from the deferred re-run", async () => { + const { out, exitCode, timedOut } = await runFixture( + "inline-snapshot", + /* js */ ` + import { test, expect } from "bun:test"; + + test("pending", async () => { + await expect(Bun.sleep(1).then(() => ({ a: 1 }))).resolves.toMatchInlineSnapshot(); + }); `, + { CI: "false" }, ); expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); expect(out).toContain("1 pass"); expect(out).toContain("0 fail"); + expect(out).toContain("+1 added"); + expect(out).not.toContain("must be called from the test file"); }, 40_000); // https://github.com/oven-sh/bun/issues/25181 From b927de1ea02bc1311cbc6c38f18be25d5fe8d846 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 01:00:59 +0000 Subject: [PATCH 04/26] Use in-flight counter instead of wall-clock for concurrent test --- .../bun/test/expect-resolves-pending.test.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index e6c1694616b..5966162d6b6 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -225,18 +225,22 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { // https://github.com/oven-sh/bun/issues/25181 // With the blocking `waitForPromise()`, each concurrent test's - // `.resolves` serialized the whole group. Ten 1s-sleeps took ~10s; - // now they overlap. + // `.resolves` serialized the whole group. Assert overlap directly + // via an in-flight counter rather than wall-clock timing. test("does not serialize test.concurrent tests", async () => { const { out, exitCode, timedOut } = await runFixture( "concurrent", /* js */ ` import { test, expect, afterAll } from "bun:test"; - const start = Date.now(); + let inFlight = 0; + let maxInFlight = 0; async function slow() { - await new Promise(r => setTimeout(r, 1000)); + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await new Promise(r => setTimeout(r, 500)); + inFlight--; return { ok: true }; } @@ -245,7 +249,7 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { }); afterAll(() => { - console.log("ELAPSED=" + (Date.now() - start)); + console.log("MAX_INFLIGHT=" + maxInFlight); }); `, ); @@ -253,11 +257,11 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { expect(timedOut).toBe(false); expect(out).toContain("10 pass"); expect(out).toContain("0 fail"); - const elapsed = Number(out.match(/ELAPSED=(\d+)/)?.[1]); - // Ten 1s-sleeps: concurrent ≈ 1s, serialized ≈ 10s. Allow generous - // slack for slow CI — anything under 5s proves they overlapped. - expect(elapsed).toBeGreaterThan(900); - expect(elapsed).toBeLessThan(5000); + // If `.resolves` blocks, each test runs to completion before the + // next starts and maxInFlight stays at 1. With the deferred path + // all ten are in flight at once. + const maxInFlight = Number(out.match(/MAX_INFLIGHT=(\d+)/)?.[1]); + expect(maxInFlight).toBe(10); expect(exitCode).toBe(0); }, 40_000); }); From 7376ca85e8ddfbdb3a0d995e9a9da7bb4e65961f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 01:16:08 +0000 Subject: [PATCH 05/26] Reject deferred promise on rerun setup failures; simplify MAX_INFLIGHT check --- src/runtime/test_runner/expect.zig | 26 +++++++++++-------- .../bun/test/expect-resolves-pending.test.ts | 3 +-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index b8cde58ff7f..908eddfcecc 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -288,21 +288,30 @@ pub const Expect = struct { fn settle(this: *PendingMatcher, globalThis: *JSGlobalObject) void { defer this.deinit(); - const expect_this = this.expect_this.get() orelse return; - const matcher_fn = this.matcher_fn.get() orelse return; - const args_array = this.matcher_args.get() orelse return; + const result = this.rerun(globalThis) catch { + const exception = globalThis.tryTakeException() orelse JSValue.js_undefined; + this.deferred.reject(globalThis, exception) catch {}; + return; + }; + this.deferred.resolve(globalThis, result) catch {}; + } + + fn rerun(this: *PendingMatcher, globalThis: *JSGlobalObject) bun.JSError!JSValue { + const expect_this = this.expect_this.get() orelse return .js_undefined; + const matcher_fn = this.matcher_fn.get() orelse return .js_undefined; + const args_array = this.matcher_args.get() orelse return .js_undefined; // Extract the original arguments back out of the array. // Most built-in matchers take 0-2 arguments, but // `toHaveBeenCalledWith` and `expect.extend()` custom matchers // are variadic, so fall back to the heap when needed. - const args_len: u32 = @intCast(args_array.getLength(globalThis) catch return); + const args_len: u32 = @intCast(try args_array.getLength(globalThis)); var sfa = std.heap.stackFallback(inline_matcher_args * @sizeOf(JSValue), bun.default_allocator); const allocator = sfa.get(); const args_buf = allocator.alloc(JSValue, args_len) catch bun.outOfMemory(); defer allocator.free(args_buf); for (0..args_len) |i| { - args_buf[i] = args_array.getIndex(globalThis, @intCast(i)) catch return; + args_buf[i] = try args_array.getIndex(globalThis, @intCast(i)); } // Mark the re-run so we don't try to defer a second time. The @@ -313,12 +322,7 @@ pub const Expect = struct { expect.is_async_rerun = false; }; - const result = matcher_fn.call(globalThis, expect_this, args_buf) catch { - const exception = globalThis.tryTakeException() orelse JSValue.js_undefined; - this.deferred.reject(globalThis, exception) catch {}; - return; - }; - this.deferred.resolve(globalThis, result) catch {}; + return matcher_fn.call(globalThis, expect_this, args_buf); } fn deinit(this: *PendingMatcher) void { diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 5966162d6b6..4801b21d73a 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -260,8 +260,7 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { // If `.resolves` blocks, each test runs to completion before the // next starts and maxInFlight stays at 1. With the deferred path // all ten are in flight at once. - const maxInFlight = Number(out.match(/MAX_INFLIGHT=(\d+)/)?.[1]); - expect(maxInFlight).toBe(10); + expect(out).toContain("MAX_INFLIGHT=10"); expect(exitCode).toBe(0); }, 40_000); }); From 626583b6bcdc8a8fa9504a0364933eabdfcf5e91 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 01:29:10 +0000 Subject: [PATCH 06/26] Reset counted_expect_call in postMatch; snapshot/restore across defer The per-Expect counted_expect_call flag was never reset, so calling two matchers on the same expect() instance only counted one assertion. Reset it in postMatch() so each matcher call starts fresh, and have PendingMatcher snapshot the flag at defer time and restore it before the re-run so the deferred path still dedups correctly regardless of whether the matcher increments before or after getValue(). --- src/runtime/test_runner/expect.zig | 38 +++++++++++++++---- .../bun/test/expect-resolves-pending.test.ts | 11 +++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index 908eddfcecc..bd204b6b491 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -27,10 +27,12 @@ pub const Expect = struct { /// Set to true while re-invoking a matcher from a `.resolves`/`.rejects` /// `.then()` callback so we don't try to defer again. is_async_rerun: bool = false, - /// Set by `incrementExpectCallCounter()` on the first call for this - /// `expect()` instance so a deferred re-run doesn't count the same - /// expectation twice. Matchers call `incrementExpectCallCounter()` - /// either before or after `getValue()`, so we can't rely on ordering. + /// Set by `incrementExpectCallCounter()` so a deferred re-run doesn't + /// count the same expectation twice. Matchers call the counter either + /// before or after `getValue()`, so we can't rely on ordering. Reset + /// by `postMatch()` so multiple matchers on the same `expect()` each + /// count; `PendingMatcher` snapshots it at defer time and restores it + /// before the re-run. counted_expect_call: bool = false, /// The caller's source location, captured when a `.resolves`/`.rejects` /// matcher is deferred. `inlineSnapshot()` needs this on the re-run @@ -224,7 +226,7 @@ pub const Expect = struct { if (this.async_rerun_srcloc) |*old| old.str.deref(); this.async_rerun_srcloc = callframe.getCallerSrcLoc(globalThis); - const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value); + const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value, this.counted_expect_call); js.resultValueSetCached(thisValue, globalThis, deferred); return deferred; } @@ -241,8 +243,13 @@ pub const Expect = struct { matcher_args: jsc.Strong.Optional, /// The promise returned to the caller of the matcher. deferred: jsc.JSPromise.Strong, + /// Whether `incrementExpectCallCounter()` had already run by the + /// time we deferred. Restored on re-run so the counter is bumped + /// exactly once regardless of whether this matcher calls it + /// before or after `getValue()`. + was_counted_before_defer: bool, - fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue) bun.JSError!JSValue { + fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue, was_counted: bool) bun.JSError!JSValue { const args = callframe.arguments(); const args_array = try JSValue.createEmptyArray(globalThis, args.len); for (args, 0..) |arg, i| { @@ -254,6 +261,7 @@ pub const Expect = struct { .matcher_fn = .create(callframe.callee(), globalThis), .matcher_args = .create(args_array, globalThis), .deferred = jsc.JSPromise.Strong.init(globalThis), + .was_counted_before_defer = was_counted, }); const deferred_value = pending.deferred.value(); @@ -317,7 +325,16 @@ pub const Expect = struct { // Mark the re-run so we don't try to defer a second time. The // captured promise is now settled, so `processPromise` will // extract its result synchronously. - if (Expect.fromJS(expect_this)) |expect| expect.is_async_rerun = true; + // + // Restore `counted_expect_call` to its value at the point of + // defer: if the matcher incremented before `getValue()` on the + // first call, the re-run's increment is a no-op; if it + // increments after, the first call never reached it and the + // re-run does. + if (Expect.fromJS(expect_this)) |expect| { + expect.is_async_rerun = true; + expect.counted_expect_call = this.was_counted_before_defer; + } defer if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = false; }; @@ -1421,7 +1438,12 @@ pub const Expect = struct { return globalThis.throw("Not implemented", .{}); } - pub fn postMatch(_: *Expect, globalThis: *JSGlobalObject) void { + pub fn postMatch(this: *Expect, globalThis: *JSGlobalObject) void { + // Each matcher call should count independently when the user + // reuses the same `expect()` instance. `PendingMatcher` snapshots + // the flag at defer time and restores it on re-run, so clearing + // here is safe in the deferred path too. + this.counted_expect_call = false; var vm = globalThis.bunVM(); vm.autoGarbageCollect(); } diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 4801b21d73a..295cceca2a2 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -191,11 +191,20 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { resolve("bar"); await assertion; }); + + // The counted_expect_call flag is per-Expect-instance; each + // matcher call on a reused instance must still count. + test("multiple matchers on the same expect() each count", () => { + expect.assertions(2); + const e = expect(5); + e.toBe(5); + e.toBeGreaterThan(0); + }); `, ); expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); - expect(out).toContain("3 pass"); + expect(out).toContain("4 pass"); expect(out).toContain("0 fail"); }, 40_000); From e3efc0695cfeab37259b8c7d848c69bbeb7b50be Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 02:03:52 +0000 Subject: [PATCH 07/26] Fix CI: use bun.handleOom; await .resolves/.rejects in tests that relied on blocking - expect.zig: replace banned `catch bun.outOfMemory()` with `bun.handleOom(...)`. - Add `await` to `.resolves`/`.rejects` assertions in four test files that previously relied on the (now-removed) synchronous waitForPromise() blocking behavior. Without the await, the test function returns before the promise settles, letting resources (servers, temp dirs, subprocesses) tear down underneath the still-running assertion. Affected: - test/js/node/fs/glob.test.ts (4 lines) - test/js/web/fetch/client-fetch.test.ts (8 lines) - test/cli/install/migration/yarn-lock-migration.test.ts (2 lines) - test/cli/install/minimum-release-age.test.ts (1 line) --- src/runtime/test_runner/expect.zig | 2 +- .../migration/yarn-lock-migration.test.ts | 4 ++-- test/cli/install/minimum-release-age.test.ts | 2 +- test/js/node/fs/glob.test.ts | 10 ++++++---- test/js/web/fetch/client-fetch.test.ts | 20 ++++++++++--------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index bd204b6b491..9cd401034b8 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -316,7 +316,7 @@ pub const Expect = struct { const args_len: u32 = @intCast(try args_array.getLength(globalThis)); var sfa = std.heap.stackFallback(inline_matcher_args * @sizeOf(JSValue), bun.default_allocator); const allocator = sfa.get(); - const args_buf = allocator.alloc(JSValue, args_len) catch bun.outOfMemory(); + const args_buf = bun.handleOom(allocator.alloc(JSValue, args_len)); defer allocator.free(args_buf); for (0..args_len) |i| { args_buf[i] = try args_array.getIndex(globalThis, @intCast(i)); diff --git a/test/cli/install/migration/yarn-lock-migration.test.ts b/test/cli/install/migration/yarn-lock-migration.test.ts index da3fbd786ef..99a2ed7063c 100644 --- a/test/cli/install/migration/yarn-lock-migration.test.ts +++ b/test/cli/install/migration/yarn-lock-migration.test.ts @@ -1366,8 +1366,8 @@ describe("bun pm migrate for existing yarn.lock", () => { stdin: "ignore", }); - expect(migrateResult.exited).resolves.toBe(0); - expect(Bun.file(join(tempDir, "bun.lock")).exists()).resolves.toBe(true); + await expect(migrateResult.exited).resolves.toBe(0); + await expect(Bun.file(join(tempDir, "bun.lock")).exists()).resolves.toBe(true); const bunLockContent = await Bun.file(join(tempDir, "bun.lock")).text(); expect(bunLockContent).toMatchSnapshot(folder); diff --git a/test/cli/install/minimum-release-age.test.ts b/test/cli/install/minimum-release-age.test.ts index d90cf6fb835..304c9c9e85a 100644 --- a/test/cli/install/minimum-release-age.test.ts +++ b/test/cli/install/minimum-release-age.test.ts @@ -1787,7 +1787,7 @@ registry = "${mockRegistryUrl}"`, stderr: "pipe", }); - expect(proc.exited).resolves.toBe(0); + await expect(proc.exited).resolves.toBe(0); const lockfile = await Bun.file(`${dir}/bun.lock`).text(); // Scoped package should be filtered (1.5.0 not 2.0.0) diff --git a/test/js/node/fs/glob.test.ts b/test/js/node/fs/glob.test.ts index 7544a36ea6e..87469c76b3d 100644 --- a/test/js/node/fs/glob.test.ts +++ b/test/js/node/fs/glob.test.ts @@ -212,23 +212,25 @@ describe("fs.promises.glob", () => { it("can filter out files", async () => { const exclude = (path: string) => path.endsWith(".js"); const expected = isWindows ? ["a\\bar.txt"] : ["a/bar.txt"]; - expect(Array.fromAsync(fs.promises.glob("a/*", { cwd: tmp, exclude }))).resolves.toStrictEqual(expected); + await expect(Array.fromAsync(fs.promises.glob("a/*", { cwd: tmp, exclude }))).resolves.toStrictEqual(expected); }); it("can filter out files (2)", async () => { const exclude = ["**/*.js"]; const expected = isWindows ? ["a\\bar.txt"] : ["a/bar.txt"]; - expect(Array.fromAsync(fs.promises.glob("a/*", { cwd: tmp, exclude }))).resolves.toStrictEqual(expected); + await expect(Array.fromAsync(fs.promises.glob("a/*", { cwd: tmp, exclude }))).resolves.toStrictEqual(expected); const exclude2 = ["folder.test/another-folder"]; const expected2 = isWindows ? ["folder.test\\file.txt"] : ["folder.test/file.txt"]; - expect( + await expect( Array.fromAsync(fs.promises.glob("folder.test/**/*", { cwd: tmp, exclude: exclude2 })), ).resolves.toStrictEqual(expected2); }); it("supports arrays of patterns", async () => { const expected = isWindows ? ["a\\bar.txt", "a\\baz.js"] : ["a/bar.txt", "a/baz.js"]; - expect(Array.fromAsync(fs.promises.glob(["a/bar.txt", "a/baz.js"], { cwd: tmp }))).resolves.toStrictEqual(expected); + await expect(Array.fromAsync(fs.promises.glob(["a/bar.txt", "a/baz.js"], { cwd: tmp }))).resolves.toStrictEqual( + expected, + ); }); }); // diff --git a/test/js/web/fetch/client-fetch.test.ts b/test/js/web/fetch/client-fetch.test.ts index a76e35d912d..643822a5af5 100644 --- a/test/js/web/fetch/client-fetch.test.ts +++ b/test/js/web/fetch/client-fetch.test.ts @@ -13,8 +13,8 @@ test("function signature", () => { }); test("args validation", async () => { - expect(fetch()).rejects.toThrow(TypeError); - expect(fetch("ftp://unsupported")).rejects.toThrow(TypeError); + await expect(fetch()).rejects.toThrow(TypeError); + await expect(fetch("ftp://unsupported")).rejects.toThrow(TypeError); }); test("request json", async () => { @@ -69,7 +69,7 @@ test("pre aborted with readable request body", async () => { const ac = new AbortController(); ac.abort(); - expect( + await expect( fetch(`http://localhost:${server.address().port}`, { signal: ac.signal, method: "POST", @@ -101,7 +101,7 @@ test("pre aborted with closed readable request body", async () => { }, }); - expect( + await expect( fetch(`http://localhost:${server.address().port}`, { signal: ac.signal, method: "POST", @@ -117,7 +117,9 @@ test("unsupported formData 1", async () => { res.end(); }).listen(0); await once(server, "listening"); - expect(fetch(`http://localhost:${server.address().port}`).then(res => res.formData())).rejects.toThrow(TypeError); + await expect(fetch(`http://localhost:${server.address().port}`).then(res => res.formData())).rejects.toThrow( + TypeError, + ); }); test("multipart formdata not base64", async () => { @@ -212,7 +214,7 @@ test("busboy emit error", async () => { await listen(0); const res = await fetch(`http://localhost:${server.address().port}`); - expect(res.formData()).rejects.toThrow("FormData parse error missing final boundary"); + await expect(res.formData()).rejects.toThrow("FormData parse error missing final boundary"); }); // https://github.com/nodejs/undici/issues/2244 @@ -268,7 +270,7 @@ test("locked blob body", async () => { const res = await fetch(`http://localhost:${server.address().port}`); const reader = res.body.getReader(); - expect(res.blob()).rejects.toThrow("ReadableStream is locked"); + await expect(res.blob()).rejects.toThrow("ReadableStream is locked"); reader.cancel(); }); @@ -280,7 +282,7 @@ test("disturbed blob body", async () => { const res = await fetch(`http://localhost:${server.address().port}`); await res.blob(); - expect(res.blob()).rejects.toThrow("Body already used"); + await expect(res.blob()).rejects.toThrow("Body already used"); }); test("redirect with body", async () => { @@ -447,7 +449,7 @@ test("error on redirect", async () => { }).listen(0); await once(server, "listening"); - expect( + await expect( fetch(`http://localhost:${server.address().port}`, { redirect: "error", }), From 459a2eaec24912f503be7c4d7ca19ed9ce784fe8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 02:34:45 +0000 Subject: [PATCH 08/26] Fix wasm-streaming crash; address remaining review findings - wasm-streaming.test.ts: add await to 20 .resolves/.rejects assertions that relied on the old blocking waitForPromise(). Without await, the WebAssembly compilation was still in flight when the process started tearing down, tripping 'ASSERTION FAILED: m_object' in ThreadSafeWeakPtr::strongRef(). Awaiting exposed a pre-existing JSC quirk where the missing-imports error message differs if the module was previously compiled in the same process; relax that one assertion to match either message. - expect.zig inlineSnapshot(): gate the captured async_rerun_srcloc on is_async_rerun, not just non-null, so a stale location from a previous deferred matcher on the same expect() isn't reused for a later synchronous call. - expect.zig applyCustomMatcher(): call postMatch() via defer (like built-in matchers do) so counted_expect_call is reset and a subsequent matcher on the same expect() instance still counts. - expect-resolves-pending.test.ts: split timedOut/out/exitCode assertions so subprocess output surfaces on non-hang failures; add a custom-then-builtin case to the expect.assertions test. --- src/runtime/test_runner/expect.zig | 14 ++++-- .../bun/test/expect-resolves-pending.test.ts | 24 +++++++--- test/js/web/fetch/wasm-streaming.test.ts | 46 ++++++++++--------- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index 9cd401034b8..0f687b6c30e 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -950,8 +950,12 @@ pub const Expect = struct { // When re-running from a deferred `.resolves`/`.rejects` // `.then()` callback, the user's frame is no longer on the // stack; use the location captured in `maybeDeferMatcher()`. - const srcloc: CallFrame.CallerSrcLoc, const owns_srcloc: bool = if (this.async_rerun_srcloc) |s| - .{ s, false } + // Gate on `is_async_rerun` (not just "is the field non-null") + // so a stale capture from a previous deferred matcher on the + // same `expect()` instance isn't reused for a later synchronous + // call, where the real callframe is available and correct. + const srcloc: CallFrame.CallerSrcLoc, const owns_srcloc: bool = if (this.is_async_rerun and this.async_rerun_srcloc != null) + .{ this.async_rerun_srcloc.?, false } else .{ callFrame.getCallerSrcLoc(globalThis), true }; defer if (owns_srcloc) srcloc.str.deref(); @@ -1316,8 +1320,6 @@ pub const Expect = struct { /// Function that is run for either `expect.myMatcher()` call or `expect().myMatcher` call, /// and we can known which case it is based on if the `callFrame.this()` value is an instance of Expect pub fn applyCustomMatcher(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!jsc.JSValue { - defer globalThis.bunVM().autoGarbageCollect(); - // retrieve the user-provided matcher function (matcher_fn) const func: JSValue = callFrame.callee(); var matcher_fn: JSValue = getCustomMatcherFn(func, globalThis) orelse .js_undefined; @@ -1329,9 +1331,13 @@ pub const Expect = struct { // try to retrieve the Expect instance const thisValue: JSValue = callFrame.this(); const expect: *Expect = Expect.fromJS(thisValue) orelse { + defer globalThis.bunVM().autoGarbageCollect(); // if no Expect instance, assume it is a static call (`expect.myMatcher()`), so create an ExpectCustomAsymmetricMatcher instance return ExpectCustomAsymmetricMatcher.create(globalThis, callFrame, matcher_fn); }; + // Use the shared cleanup so `counted_expect_call` is reset the same + // way built-in matchers do via `defer this.postMatch(...)`. + defer expect.postMatch(globalThis); // if we got an Expect instance, then it's a non-static call (`expect().myMatcher`), // so now execute the symmetric matching diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 295cceca2a2..e8bd7fdab48 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -110,10 +110,11 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { `, ); - expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); + expect(timedOut).toBe(false); expect(out).toContain("8 pass"); expect(out).toContain("0 fail"); expect(out).not.toContain("timed out"); + expect(exitCode).toBe(0); }, 40_000); test("a failing deferred assertion still fails the test", async () => { @@ -144,13 +145,14 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { `, ); - expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 1 }); + expect(timedOut).toBe(false); expect(out).toContain("0 pass"); expect(out).toContain("3 fail"); expect(out).toContain("Expected: 25"); expect(out).toContain("Received: 99"); expect(out).toContain("Expected promise that rejects"); expect(out).not.toContain("timed out"); + expect(exitCode).toBe(1); }, 40_000); // Matchers are inconsistent about whether they call @@ -193,19 +195,28 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { }); // The counted_expect_call flag is per-Expect-instance; each - // matcher call on a reused instance must still count. + // matcher call on a reused instance must still count, including + // when the first is a custom matcher (applyCustomMatcher path). test("multiple matchers on the same expect() each count", () => { expect.assertions(2); const e = expect(5); e.toBe(5); e.toBeGreaterThan(0); }); + + test("custom then built-in on the same expect() each count", () => { + expect.assertions(2); + const e = expect("bar"); + e.toBeBar(); + e.toBe("bar"); + }); `, ); - expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); - expect(out).toContain("4 pass"); + expect(timedOut).toBe(false); + expect(out).toContain("5 pass"); expect(out).toContain("0 fail"); + expect(exitCode).toBe(0); }, 40_000); // On the deferred re-run the user's frame is gone from the stack, so @@ -225,11 +236,12 @@ describe("expect().resolves / .rejects on a still-pending promise", () => { { CI: "false" }, ); - expect({ timedOut, exitCode }).toEqual({ timedOut: false, exitCode: 0 }); + expect(timedOut).toBe(false); expect(out).toContain("1 pass"); expect(out).toContain("0 fail"); expect(out).toContain("+1 added"); expect(out).not.toContain("must be called from the test file"); + expect(exitCode).toBe(0); }, 40_000); // https://github.com/oven-sh/bun/issues/25181 diff --git a/test/js/web/fetch/wasm-streaming.test.ts b/test/js/web/fetch/wasm-streaming.test.ts index d0a53ebed83..2dd699cba08 100644 --- a/test/js/web/fetch/wasm-streaming.test.ts +++ b/test/js/web/fetch/wasm-streaming.test.ts @@ -48,18 +48,18 @@ const responseFromStream = (pull: (controller: ReadableStreamDefaultController { test("compiles a non-streaming Response", async () => { const response = await fetch(simpleWasmUri); - expect(WebAssembly.compileStreaming(response)).resolves.toBeInstanceOf(WebAssembly.Module); + await expect(WebAssembly.compileStreaming(response)).resolves.toBeInstanceOf(WebAssembly.Module); }); test("compiles a resolved Promise to a non-streaming Response", async () => { const promise = Promise.resolve(await fetch(simpleWasmUri)); - expect(WebAssembly.compileStreaming(promise)).resolves.toBeInstanceOf(WebAssembly.Module); + await expect(WebAssembly.compileStreaming(promise)).resolves.toBeInstanceOf(WebAssembly.Module); }); test("compiles a pending Promise to a non-streaming Response", async () => { const response = await fetch(simpleWasmUri); const promise = Bun.sleep(100).then(() => response); - expect(WebAssembly.compileStreaming(promise)).resolves.toBeInstanceOf(WebAssembly.Module); + await expect(WebAssembly.compileStreaming(promise)).resolves.toBeInstanceOf(WebAssembly.Module); }); // Errors: @@ -67,20 +67,20 @@ describe("WebAssembly.compileStreaming", () => { test("doesn't compile a rejected Promise", async () => { const error = new Error("sudden explosion"); const promise = Promise.reject(error); - expect(WebAssembly.compileStreaming(promise)).rejects.toBe(error); + await expect(WebAssembly.compileStreaming(promise)).rejects.toBe(error); }); test("doesn't compile a non-Response", async () => { const nonResponse = Buffer.from("not a Response"); // @ts-expect-error nonResponse is not a Response - expect(WebAssembly.compileStreaming(nonResponse)).rejects.toThrow( + await expect(WebAssembly.compileStreaming(nonResponse)).rejects.toThrow( `The "source" argument must be an instance of Response or an Promise resolving to Response. Received an instance of Buffer`, ); }); test("doesn't compile a response with the wrong MIME type", async () => { const response = await fetch("data:image/png;base64," + simpleWasm); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow( + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow( "WebAssembly response has unsupported MIME type 'image/png'", ); }); @@ -93,7 +93,7 @@ describe("WebAssembly.compileStreaming", () => { status: 418, }); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow("WebAssembly response has status code 418"); + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow("WebAssembly response has status code 418"); }); test("doesn't compile a used streaming response", async () => { @@ -108,7 +108,9 @@ describe("WebAssembly.compileStreaming", () => { for await (const _ of response.body); // Consume the stream ok(response.bodyUsed); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow("WebAssembly response body has already been used"); + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow( + "WebAssembly response body has already been used", + ); }); test("doesn't compile a streaming response that throws while streaming", async () => { @@ -120,7 +122,7 @@ describe("WebAssembly.compileStreaming", () => { i++; }); - expect(WebAssembly.compileStreaming(response)).rejects.toBe(error); + await expect(WebAssembly.compileStreaming(response)).rejects.toBe(error); }); test("doesn't compile a streaming response that yields neither ArrayBuffer nor ArrayBufferView", async () => { @@ -128,7 +130,7 @@ describe("WebAssembly.compileStreaming", () => { controller.enqueue("something random"); }); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow( + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow( "chunk must be an ArrayBufferView or an ArrayBuffer", ); }); @@ -140,7 +142,7 @@ describe("WebAssembly.compileStreaming", () => { controller.enqueue(array); }); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow( + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow( "Underlying ArrayBuffer has been detached from the view or out-of-bounds", ); }); @@ -152,14 +154,14 @@ describe("WebAssembly.compileStreaming", () => { controller.enqueue(buffer); }); - expect(WebAssembly.compileStreaming(response)).rejects.toThrow( + await expect(WebAssembly.compileStreaming(response)).rejects.toThrow( "Underlying ArrayBuffer has been detached from the view or out-of-bounds", ); }); test("doesn't compile a response that isn't valid WebAssembly", async () => { const response = await fetch("data:application/wasm,This is not actually Wasm"); - expect(WebAssembly.compileStreaming(response)).rejects.toBeInstanceOf(WebAssembly.CompileError); + await expect(WebAssembly.compileStreaming(response)).rejects.toBeInstanceOf(WebAssembly.CompileError); }); }); @@ -180,18 +182,18 @@ describe("WebAssembly.instantiateStreaming", () => { test("instantiates a non-streaming response", async () => { const response = await fetch(simpleWasmUri); - expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); + await expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); }); test("instantiates a non-streaming response, without an import object", async () => { const response = await fetch(simplerWasmUri); - expect(instantiateAndGetExports(response)).resolves.toHaveProperty("add"); + await expect(instantiateAndGetExports(response)).resolves.toHaveProperty("add"); }); test("instantiates a pending Promise to a non-streaming response", async () => { const response = await fetch(simpleWasmUri); const promise = Bun.sleep(100).then(() => response); - expect(instantiateAndGetExports(promise, imports)).resolves.toHaveProperty("div"); + await expect(instantiateAndGetExports(promise, imports)).resolves.toHaveProperty("div"); }); test("instantiates a Bun.file() response", async () => { @@ -199,7 +201,7 @@ describe("WebAssembly.instantiateStreaming", () => { await Bun.write(path, Buffer.from(simpleWasm, "base64")); const response = new Response(Bun.file(path)); - expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); + await expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); }); test("instantiates a ReadableStream response", async () => { @@ -215,7 +217,7 @@ describe("WebAssembly.instantiateStreaming", () => { if (i >= buffer.length) controller.close(); }); - expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); + await expect(instantiateAndGetExports(response, imports)).resolves.toHaveProperty("div"); }); test("instantiates a string response", async () => { @@ -225,15 +227,17 @@ describe("WebAssembly.instantiateStreaming", () => { }, }); - expect(instantiateAndGetExports(response)).resolves.toHaveProperty("foo"); + await expect(instantiateAndGetExports(response)).resolves.toHaveProperty("foo"); }); // Errors: test("doesn't instantiate a response without the correct import object", async () => { const response = await fetch(simpleWasmUri); - expect(instantiateAndGetExports(response)).rejects.toThrow( - "can't make WebAssembly.Instance because there is no imports Object and the WebAssembly.Module requires imports", + // JSC produces a different message depending on whether this module + // was previously compiled in the same process, so accept either. + await expect(instantiateAndGetExports(response)).rejects.toThrow( + /there is no imports Object and the WebAssembly\.Module requires imports|import env:reciprocal must be an object/, ); }); }); From d57797e1bbbf58478710e34f60a81c32d734fd1b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 02:48:05 +0000 Subject: [PATCH 09/26] Address remaining review: await fs.watch/worker_blob; snapshot flags; concurrent subprocess tests - fs.watch.test.ts (4 lines) and worker_blob.test.ts (1 line): add await to .resolves/.rejects assertions on genuinely-pending promises (fs.watch callbacks, Worker error events). Without await these tests now pass vacuously and leak intervals/watchers into later tests. - PendingMatcher: snapshot Expect.flags at defer time and restore around the re-run. .not/.resolves/.rejects mutate flags in place on the shared Expect, so a later matcher call on a reused instance could flip flags.not before an earlier call's re-run fires. - expect-resolves-pending.test.ts: describe.concurrent for the five independent subprocess tests (3.4s vs 6.4s locally; 20s vs 100s on a regressed build). --- src/runtime/test_runner/expect.zig | 29 ++++++++++++++----- .../bun/test/expect-resolves-pending.test.ts | 2 +- test/js/node/watch/fs.watch.test.ts | 8 ++--- test/js/web/workers/worker_blob.test.ts | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index 0f687b6c30e..fe024eee6ec 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -226,7 +226,7 @@ pub const Expect = struct { if (this.async_rerun_srcloc) |*old| old.str.deref(); this.async_rerun_srcloc = callframe.getCallerSrcLoc(globalThis); - const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value, this.counted_expect_call); + const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value, this.counted_expect_call, this.flags); js.resultValueSetCached(thisValue, globalThis, deferred); return deferred; } @@ -248,8 +248,14 @@ pub const Expect = struct { /// exactly once regardless of whether this matcher calls it /// before or after `getValue()`. was_counted_before_defer: bool, - - fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue, was_counted: bool) bun.JSError!JSValue { + /// The `Expect.flags` at the time we deferred. `.not` / `.resolves` + /// / `.rejects` mutate flags in place on the shared instance, so a + /// later matcher call on the same `expect()` could flip them before + /// this re-run fires; restore so the re-run observes the flags the + /// user wrote for *this* matcher call. + flags_at_defer: Expect.Flags, + + fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue, was_counted: bool, flags: Expect.Flags) bun.JSError!JSValue { const args = callframe.arguments(); const args_array = try JSValue.createEmptyArray(globalThis, args.len); for (args, 0..) |arg, i| { @@ -262,6 +268,7 @@ pub const Expect = struct { .matcher_args = .create(args_array, globalThis), .deferred = jsc.JSPromise.Strong.init(globalThis), .was_counted_before_defer = was_counted, + .flags_at_defer = flags, }); const deferred_value = pending.deferred.value(); @@ -326,17 +333,23 @@ pub const Expect = struct { // captured promise is now settled, so `processPromise` will // extract its result synchronously. // - // Restore `counted_expect_call` to its value at the point of - // defer: if the matcher incremented before `getValue()` on the - // first call, the re-run's increment is a no-op; if it - // increments after, the first call never reached it and the - // re-run does. + // Restore `counted_expect_call` and `flags` to their values at + // the point of defer: if the matcher incremented before + // `getValue()` on the first call, the re-run's increment is a + // no-op; if it increments after, the first call never reached + // it and the re-run does. `flags` is restored (and the current + // value put back afterwards) so a later `.not` on the same + // reused `expect()` instance doesn't leak into this re-run. + var saved_flags: Expect.Flags = undefined; if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = true; expect.counted_expect_call = this.was_counted_before_defer; + saved_flags = expect.flags; + expect.flags = this.flags_at_defer; } defer if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = false; + expect.flags = saved_flags; }; return matcher_fn.call(globalThis, expect_this, args_buf); diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index e8bd7fdab48..c2a99e89977 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -33,7 +33,7 @@ async function runFixture(name: string, source: string, extraEnv: Record { +describe.concurrent("expect().resolves / .rejects on a still-pending promise", () => { // Exact reproduction from #14950, plus the .rejects mirror and several // matcher variants. test("does not hang when the promise is settled after the matcher call", async () => { diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts index 51d65271e28..f447d121a63 100644 --- a/test/js/node/watch/fs.watch.test.ts +++ b/test/js/node/watch/fs.watch.test.ts @@ -494,7 +494,7 @@ describe("fs.watch", () => { reject("timeout"); }, 3000); }); - expect(promise).resolves.toBe("change"); + await expect(promise).resolves.toBe("change"); }); // on windows 0o200 will be readable (match nodejs behavior) @@ -681,7 +681,7 @@ describe("fs.promises.watch", () => { clearInterval(interval); } })(); - expect(promise).resolves.toBe("rename"); + await expect(promise).resolves.toBe("rename"); }); test("should work with symlink dir", async () => { @@ -709,7 +709,7 @@ describe("fs.promises.watch", () => { clearInterval(interval); } })(); - expect(promise).resolves.toBe("rename"); + await expect(promise).resolves.toBe("rename"); }); test("should work with symlink", async () => { @@ -732,7 +732,7 @@ describe("fs.promises.watch", () => { clearInterval(interval); } })(); - expect(promise).resolves.toBe("change"); + await expect(promise).resolves.toBe("change"); }); }); diff --git a/test/js/web/workers/worker_blob.test.ts b/test/js/web/workers/worker_blob.test.ts index aaf58ae0ea3..efe85577ac9 100644 --- a/test/js/web/workers/worker_blob.test.ts +++ b/test/js/web/workers/worker_blob.test.ts @@ -60,7 +60,7 @@ test("Worker from a blob errors on invalid blob", async () => { const { promise, reject } = Promise.withResolvers(); const worker = new Worker("blob:i dont exist!"); worker.addEventListener("error", e => reject(e.message)); - expect(promise).rejects.toBe('BuildMessage: ModuleNotFound resolving "blob:i dont exist!" (entry point)'); + await expect(promise).rejects.toBe('BuildMessage: ModuleNotFound resolving "blob:i dont exist!" (entry point)'); }); test("Revoking an object URL after a Worker is created before it loads should throw an error", async () => { From ac962ebebbfdf36c503f64d0e818e2bb959773dc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 03:09:36 +0000 Subject: [PATCH 10/26] Store deferred srcloc per PendingMatcher, not on shared Expect Two deferred .resolves/.rejects matchers on the same expect() instance would previously share async_rerun_srcloc, so a later defer overwrote the earlier one's call site before its re-run. Move the captured srcloc into PendingMatcher (alongside flags_at_defer and was_counted_before_defer) and lend it to Expect.async_rerun_srcloc only for the duration of rerun(). PendingMatcher now owns the srcloc string; Expect.finalize no longer derefs it. Test: two deferred toMatchInlineSnapshot() on one expect() each write to their own line. --- src/runtime/test_runner/expect.zig | 43 ++++++++++++------- .../bun/test/expect-resolves-pending.test.ts | 17 +++++++- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index fe024eee6ec..c54ac584ac8 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -34,9 +34,12 @@ pub const Expect = struct { /// count; `PendingMatcher` snapshots it at defer time and restores it /// before the re-run. counted_expect_call: bool = false, - /// The caller's source location, captured when a `.resolves`/`.rejects` - /// matcher is deferred. `inlineSnapshot()` needs this on the re-run - /// because the user's frame is no longer on the stack. + /// The caller's source location for the currently-executing deferred + /// re-run, borrowed from `PendingMatcher.srcloc_at_defer`. Written by + /// `PendingMatcher.rerun()` for the duration of the re-invoked matcher + /// call and restored afterwards. `inlineSnapshot()` reads it (gated on + /// `is_async_rerun`) because the user's frame is no longer on the stack. + /// Owned by the `PendingMatcher`, not by `Expect`. async_rerun_srcloc: ?CallFrame.CallerSrcLoc = null, pub const TestScope = struct { @@ -219,13 +222,6 @@ pub const Expect = struct { promise.setHandled(globalThis.vm()); - // Capture the caller's source location now, while the user's frame - // is still on the stack. `toMatchInlineSnapshot` / - // `toThrowErrorMatchingInlineSnapshot` need it on the re-run to - // know which line to write the snapshot back to. - if (this.async_rerun_srcloc) |*old| old.str.deref(); - this.async_rerun_srcloc = callframe.getCallerSrcLoc(globalThis); - const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value, this.counted_expect_call, this.flags); js.resultValueSetCached(thisValue, globalThis, deferred); return deferred; @@ -254,6 +250,13 @@ pub const Expect = struct { /// this re-run fires; restore so the re-run observes the flags the /// user wrote for *this* matcher call. flags_at_defer: Expect.Flags, + /// The caller's source location at the time we deferred, captured + /// while the user's frame is still on the stack. `inlineSnapshot()` + /// needs it on the re-run to know which line to write the snapshot + /// back to. Stored per-PendingMatcher (not on the shared `Expect`) + /// so two deferred inline-snapshot matchers on the same `expect()` + /// instance each write to their own call site. + srcloc_at_defer: CallFrame.CallerSrcLoc, fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue, was_counted: bool, flags: Expect.Flags) bun.JSError!JSValue { const args = callframe.arguments(); @@ -269,6 +272,7 @@ pub const Expect = struct { .deferred = jsc.JSPromise.Strong.init(globalThis), .was_counted_before_defer = was_counted, .flags_at_defer = flags, + .srcloc_at_defer = callframe.getCallerSrcLoc(globalThis), }); const deferred_value = pending.deferred.value(); @@ -340,16 +344,24 @@ pub const Expect = struct { // it and the re-run does. `flags` is restored (and the current // value put back afterwards) so a later `.not` on the same // reused `expect()` instance doesn't leak into this re-run. + // `async_rerun_srcloc` is borrowed to this `PendingMatcher`'s + // captured location for the duration of the call and restored + // afterwards so two deferred matchers on the same `expect()` + // each see their own call site in `inlineSnapshot()`. var saved_flags: Expect.Flags = undefined; + var saved_srcloc: ?CallFrame.CallerSrcLoc = null; if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = true; expect.counted_expect_call = this.was_counted_before_defer; saved_flags = expect.flags; expect.flags = this.flags_at_defer; + saved_srcloc = expect.async_rerun_srcloc; + expect.async_rerun_srcloc = this.srcloc_at_defer; } defer if (Expect.fromJS(expect_this)) |expect| { expect.is_async_rerun = false; expect.flags = saved_flags; + expect.async_rerun_srcloc = saved_srcloc; }; return matcher_fn.call(globalThis, expect_this, args_buf); @@ -360,6 +372,7 @@ pub const Expect = struct { this.matcher_fn.deinit(); this.matcher_args.deinit(); this.deferred.deinit(); + this.srcloc_at_defer.str.deref(); bun.destroy(this); } @@ -529,7 +542,8 @@ pub const Expect = struct { ) callconv(.c) void { this.custom_label.deref(); if (this.parent) |parent| parent.deref(); - if (this.async_rerun_srcloc) |*s| s.str.deref(); + // `async_rerun_srcloc` is borrowed from `PendingMatcher` for the + // duration of a re-run and is not owned here; don't deref. VirtualMachine.get().allocator.destroy(this); } @@ -962,11 +976,8 @@ pub const Expect = struct { // 1. find the src loc of the snapshot // When re-running from a deferred `.resolves`/`.rejects` // `.then()` callback, the user's frame is no longer on the - // stack; use the location captured in `maybeDeferMatcher()`. - // Gate on `is_async_rerun` (not just "is the field non-null") - // so a stale capture from a previous deferred matcher on the - // same `expect()` instance isn't reused for a later synchronous - // call, where the real callframe is available and correct. + // stack; `PendingMatcher.rerun()` lends its captured location + // via `async_rerun_srcloc` for the duration of the call. const srcloc: CallFrame.CallerSrcLoc, const owns_srcloc: bool = if (this.is_async_rerun and this.async_rerun_srcloc != null) .{ this.async_rerun_srcloc.?, false } else diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index c2a99e89977..4ba1286ccda 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -223,6 +223,10 @@ describe.concurrent("expect().resolves / .rejects on a still-pending promise", ( // `inlineSnapshot()` has to use the source location captured on the // first call. Only the write path reads the source location, so this // exercises it by creating a new snapshot (hence `CI: "false"`). + // + // The second test reuses the same `expect()` instance for two deferred + // snapshots. Each must write to its own call site — the srcloc is + // stored per-PendingMatcher, not on the shared `Expect`. test("toMatchInlineSnapshot writes from the deferred re-run", async () => { const { out, exitCode, timedOut } = await runFixture( "inline-snapshot", @@ -232,14 +236,23 @@ describe.concurrent("expect().resolves / .rejects on a still-pending promise", ( test("pending", async () => { await expect(Bun.sleep(1).then(() => ({ a: 1 }))).resolves.toMatchInlineSnapshot(); }); + + test("two on the same expect()", async () => { + let resolve; + const e = expect(new Promise(r => (resolve = r))); + const first = e.resolves.toMatchInlineSnapshot(); + const second = e.resolves.toMatchInlineSnapshot(); + resolve("x"); + await Promise.all([first, second]); + }); `, { CI: "false" }, ); expect(timedOut).toBe(false); - expect(out).toContain("1 pass"); + expect(out).toContain("2 pass"); expect(out).toContain("0 fail"); - expect(out).toContain("+1 added"); + expect(out).toContain("+3 added"); expect(out).not.toContain("must be called from the test file"); expect(exitCode).toBe(0); }, 40_000); From 4ddd4730d50fa73b818cfaa92e92a20badad3963 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 03:48:12 +0000 Subject: [PATCH 11/26] Await remaining .resolves/.rejects on pending promises in test suite Completes the sweep started in 8d3e0147 and extended in 4c3a38c6/7c2c7375. These tests had un-awaited `expect(p).resolves/.rejects.X()` calls on genuinely-pending promises (response.text(), fetch(), dns.lookup(), import(), file.unlink(), WebSocket open, etc.) that previously relied on waitForPromise() blocking until settlement. With the deferred-matcher path the assertion now runs later, so: - serve.test.ts: subprocess.kill() / server dispose raced ahead of body reads; CI failure on x64-asan. - inspect.test.ts: webSocket.send() fired before the open event; CI failure on 3 Windows lanes. - resolve-dns.test.ts / worker_threads.test.ts / resolve.test.ts: sync test callbacks returned before the assertion ran; tests passed vacuously. Intentionally left alone: - expect.test.js:4675/4678 (already-settled promises still take the synchronous path) - client-fetch.test.ts:500 (value is a Response, not a Promise) - pidfd-exit-nested-tick.test.ts (the line was the nested-tick trigger, not an assertion; added a comment noting the trigger no longer fires) --- test/cli/inspect/inspect.test.ts | 4 +-- test/cli/install/bun-pm-version.test.ts | 8 ++--- test/cli/run/run-crash-handler.test.ts | 2 +- test/js/bun/dns/resolve-dns.test.ts | 8 ++--- test/js/bun/http/bun-serve-routes.test.ts | 2 +- test/js/bun/http/proxy.test.ts | 2 +- test/js/bun/http/serve.test.ts | 16 ++++----- test/js/bun/resolve/resolve.test.ts | 36 +++++++++---------- .../bun/spawn/pidfd-exit-nested-tick.test.ts | 7 ++++ test/js/node/events/event-emitter.test.ts | 4 +-- .../worker_threads/worker_threads.test.ts | 4 +-- test/js/third_party/stripe/stripe.test.ts | 2 +- test/js/valkey/valkey.test.ts | 2 +- test/js/web/fetch/blob-write.test.ts | 4 +-- test/js/web/fetch/fetch.test.ts | 32 ++++++++--------- .../fetch/form-data-boundary-crash.test.ts | 10 +++--- test/js/web/streams/streams.test.js | 4 +-- test/regression/issue/06443.test.ts | 2 +- 18 files changed, 78 insertions(+), 71 deletions(-) diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts index 41315f70310..dc3a9a50926 100644 --- a/test/cli/inspect/inspect.test.ts +++ b/test/cli/inspect/inspect.test.ts @@ -263,7 +263,7 @@ describe("websocket", () => { }).toMatchObject(expected); const webSocket = new WebSocket(url); - expect( + await expect( new Promise((resolve, reject) => { webSocket.addEventListener("open", () => resolve()); webSocket.addEventListener("error", cause => reject(new Error("WebSocket error", { cause }))); @@ -272,7 +272,7 @@ describe("websocket", () => { ).resolves.toBeUndefined(); webSocket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression: "1 + 1" } })); - expect( + await expect( new Promise(resolve => { webSocket.addEventListener("message", ({ data }) => { resolve(JSON.parse(data.toString())); diff --git a/test/cli/install/bun-pm-version.test.ts b/test/cli/install/bun-pm-version.test.ts index 1021f4df3ea..4e96dc6a410 100644 --- a/test/cli/install/bun-pm-version.test.ts +++ b/test/cli/install/bun-pm-version.test.ts @@ -717,8 +717,8 @@ describe.concurrent("bun pm version", () => { stdout: "ignore", }).exited; - expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); + await expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); + await expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); const eventContent = await Bun.file(join(testDir2, "event.log")).text(); const scriptContent = await Bun.file(join(testDir2, "script.log")).text(); @@ -777,8 +777,8 @@ describe.concurrent("bun pm version", () => { stdout: "ignore", }).exited; - expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); + await expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); + await expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); const content = await Bun.file(join(testDir4, "version-output.txt")).text(); expect(content.trim()).toBe("built"); diff --git a/test/cli/run/run-crash-handler.test.ts b/test/cli/run/run-crash-handler.test.ts index 76396616814..6475315d850 100644 --- a/test/cli/run/run-crash-handler.test.ts +++ b/test/cli/run/run-crash-handler.test.ts @@ -118,7 +118,7 @@ test("raise ignoring panic handler does not trigger the panic handler", async () /// Wait two seconds for a slow http request, or continue immediately once the request is heard. await Promise.race([resolve_handler.promise, Bun.sleep(2000)]); - expect(proc.exited).resolves.not.toBe(0); + await expect(proc.exited).resolves.not.toBe(0); expect(sent).toBe(false); }); diff --git a/test/js/bun/dns/resolve-dns.test.ts b/test/js/bun/dns/resolve-dns.test.ts index c78284e8621..30354578723 100644 --- a/test/js/bun/dns/resolve-dns.test.ts +++ b/test/js/bun/dns/resolve-dns.test.ts @@ -96,17 +96,17 @@ describe("dns", () => { } }); }); - test.each(invalidHostnames)("%s", hostname => { + test.each(invalidHostnames)("%s", async hostname => { // @ts-expect-error - expect(dns.lookup(hostname, { backend })).rejects.toMatchObject({ + await expect(dns.lookup(hostname, { backend })).rejects.toMatchObject({ code: "DNS_ENOTFOUND", name: "DNSException", }); }); - test.each(malformedHostnames)("'%s'", hostname => { + test.each(malformedHostnames)("'%s'", async hostname => { // @ts-expect-error - expect(dns.lookup(hostname, { backend })).rejects.toMatchObject({ + await expect(dns.lookup(hostname, { backend })).rejects.toMatchObject({ code: expect.stringMatching(/^DNS_ENOTFOUND|DNS_ESERVFAIL|DNS_ENOTIMP$/), name: "DNSException", }); diff --git a/test/js/bun/http/bun-serve-routes.test.ts b/test/js/bun/http/bun-serve-routes.test.ts index e3f70535d6a..6c3151260c8 100644 --- a/test/js/bun/http/bun-serve-routes.test.ts +++ b/test/js/bun/http/bun-serve-routes.test.ts @@ -621,7 +621,7 @@ it("don't crash on server.fetch()", async () => { routes: { "/test": () => new Response("test") }, }); - expect(server.fetch("/test")).rejects.toThrow("fetch() requires the server to have a fetch handler"); + await expect(server.fetch("/test")).rejects.toThrow("fetch() requires the server to have a fetch handler"); }); it("route precedence for any routes", async () => { diff --git a/test/js/bun/http/proxy.test.ts b/test/js/bun/http/proxy.test.ts index 96478976974..2ef426ce223 100644 --- a/test/js/bun/http/proxy.test.ts +++ b/test/js/bun/http/proxy.test.ts @@ -369,7 +369,7 @@ test("non-TLS origin redirect through HTTPS proxy forwards every hop through the }); test("unsupported protocol", async () => { - expect( + await expect( fetch("https://httpbin.org/get", { proxy: "ftp://asdf.com", }), diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index b31e07eeeaa..914eb542cdc 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -489,7 +489,7 @@ describe("streaming", () => { const response = await fetch(url); expect(response.status).toBe(402); expect(response.headers.get("X-Hey")).toBe("123"); - expect(response.text()).resolves.toBe(""); + await expect(response.text()).resolves.toBe(""); subprocess.kill(); }); @@ -514,7 +514,7 @@ describe("streaming", () => { const response = await fetch(url); expect(response.status).toBe(402); expect(response.headers.get("X-Hey")).toBe("123"); - expect(response.text()).resolves.toBe(""); + await expect(response.text()).resolves.toBe(""); subprocess.kill(); }); @@ -1617,7 +1617,7 @@ it("should support promise returned from error", async () => { { const resp = await fetch(new URL("async-fulfilled", url)); expect(resp.status).toBe(200); - expect(resp.text()).resolves.toBe("Async fulfilled"); + await expect(resp.text()).resolves.toBe("Async fulfilled"); } { @@ -1628,7 +1628,7 @@ it("should support promise returned from error", async () => { { const resp = await fetch(new URL("async-pending", url)); expect(resp.status).toBe(200); - expect(resp.text()).resolves.toBe("Async pending"); + await expect(resp.text()).resolves.toBe("Async pending"); } { @@ -1754,7 +1754,7 @@ it.concurrent("should work with dispose keyword", async () => { url = server.url; expect((await fetch(url)).status).toBe(200); } - expect(fetch(url)).rejects.toThrow(); + await expect(fetch(url)).rejects.toThrow(); }); // prettier-ignore @@ -2026,9 +2026,9 @@ it.concurrent( const res = await fetch(new URL(pathname, server.url.origin)); expect(res.status).toBe(200); if (success) { - expect(res.text()).resolves.toBe("Hello, World!"); + await expect(res.text()).resolves.toBe("Hello, World!"); } else { - expect(res.text()).rejects.toThrow(/The socket connection was closed unexpectedly./); + await expect(res.text()).rejects.toThrow(/The socket connection was closed unexpectedly./); } } await Promise.all([testTimeout("/ok", true), testTimeout("/timeout", false)]); @@ -2124,7 +2124,7 @@ it.concurrent( expect(server.timeout).toBeFunction(); const res = await fetch(new URL("/long-timeout", server.url.origin)); expect(res.status).toBe(200); - expect(res.text()).resolves.toBe("Hello, World!"); + await expect(res.text()).resolves.toBe("Hello, World!"); }, 20_000, ); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index 46fe4783d66..5346cf10ad3 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -314,24 +314,24 @@ it.todo("import override to bun:test", async () => { expect(await import("#bun_test")).toBeDefined(); }); -it.if(isWindows)("directory cache key computation", () => { - expect(import(`${process.cwd()}\\\\doesnotexist.ts`)).rejects.toThrow(); - expect(import(`${process.cwd()}\\\\\\doesnotexist.ts`)).rejects.toThrow(); - expect(import(`\\\\Test\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\doesnotexist.ts\\\\\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\doesnotexist.ts` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\\\doesnotexist.ts` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\doesnotexist.ts` as any)).rejects.toThrow(); - expect(import(`\\\\\\Test\\doesnotexist.ts` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\doesnotexist.ts\\` as any)).rejects.toThrow(); - expect(import(`\\\\\\Test\\doesnotexist.ts\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\\\\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); - expect(import(`\\\\Test\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); - expect(import(`\\\\\\Test\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); +it.if(isWindows)("directory cache key computation", async () => { + await expect(import(`${process.cwd()}\\\\doesnotexist.ts`)).rejects.toThrow(); + await expect(import(`${process.cwd()}\\\\\\doesnotexist.ts`)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\doesnotexist.ts\\\\\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\doesnotexist.ts` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\\\doesnotexist.ts` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\doesnotexist.ts` as any)).rejects.toThrow(); + await expect(import(`\\\\\\Test\\doesnotexist.ts` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\\\doesnotexist.ts\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\doesnotexist.ts\\` as any)).rejects.toThrow(); + await expect(import(`\\\\\\Test\\doesnotexist.ts\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\\\\\\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); + await expect(import(`\\\\Test\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); + await expect(import(`\\\\\\Test\\doesnotexist.ts\\\\` as any)).rejects.toThrow(); }); describe("NODE_PATH test", () => { diff --git a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts index dbebd6c854b..0730ce4e15c 100644 --- a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts +++ b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts @@ -52,6 +52,13 @@ test.skipIf(!isLinux)( // sibling pidfd events queued after this one in the outer batch // are dropped. With EPOLLONESHOT those pidfds are now disarmed in // the kernel with no re-arm path. + // + // Note: since #30595, `.resolves` on a still-pending promise no + // longer calls `waitForPromise`, so this line no longer forces + // the synchronous nested tick that originally triggered the bug. + // The test still validates that every child fires onExit (the + // level-triggered pidfd registration makes that robust regardless + // of whether the nested-tick drop path is exercised). if (!nested) { nested = true; expect(Bun.sleep(1)).resolves.toBe(undefined); diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index aae4efd5156..04964c04fd8 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -859,7 +859,7 @@ test("addAbortListener", async () => { const mocked = mock(); EventEmitter.addAbortListener(controller.signal, mocked); controller.abort(); - expect(promise).rejects.toThrow("aborted"); + await expect(promise).rejects.toThrow("aborted"); expect(mocked).toHaveBeenCalled(); }); @@ -872,7 +872,7 @@ test("using addAbortListener", async () => { using aborty = EventEmitter.addAbortListener(controller.signal, mocked); } controller.abort(); - expect(promise).rejects.toThrow("aborted"); + await expect(promise).rejects.toThrow("aborted"); expect(mocked).not.toHaveBeenCalled(); }); diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 555c205c6fc..3cb2ee32759 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -443,9 +443,9 @@ describe("getHeapSnapshot", () => { }); }); - test("returns a rejected promise if the worker is not running", () => { + test("returns a rejected promise if the worker is not running", async () => { const worker = new Worker("", { eval: true }); - expect(worker.getHeapSnapshot()).rejects.toMatchObject({ + await expect(worker.getHeapSnapshot()).rejects.toMatchObject({ name: "Error", code: "ERR_WORKER_NOT_RUNNING", message: "Worker instance not running", diff --git a/test/js/third_party/stripe/stripe.test.ts b/test/js/third_party/stripe/stripe.test.ts index 17fc728a4dc..7ae56973850 100644 --- a/test/js/third_party/stripe/stripe.test.ts +++ b/test/js/third_party/stripe/stripe.test.ts @@ -9,7 +9,7 @@ describe.skipIf(!stripeCredentials)("stripe", () => { const stripe = new Stripe(accessToken); test("should be able to query a charge", async () => { - expect(stripe.charges.retrieve(chargeId, { stripeAccount: accountId })).rejects.toThrow( + await expect(stripe.charges.retrieve(chargeId, { stripeAccount: accountId })).rejects.toThrow( `No such charge: '${chargeId}'`, ); }); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 6f0364d6ca3..3024be11485 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -6300,7 +6300,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); await subscriber.unsubscribe(channel); - expect(ctx.redis.set(testKey(), testValue())).resolves.toEqual("OK"); + await expect(ctx.redis.set(testKey(), testValue())).resolves.toEqual("OK"); }); test("subscribing to a channel receives messages", async () => { diff --git a/test/js/web/fetch/blob-write.test.ts b/test/js/web/fetch/blob-write.test.ts index 9fc61c0a110..16453750f6b 100644 --- a/test/js/web/fetch/blob-write.test.ts +++ b/test/js/web/fetch/blob-write.test.ts @@ -32,14 +32,14 @@ test("blob.delete() throws for data-backed blob", () => { test("Bun.file(path).unlink() does not throw", async () => { const dir = tempDirWithFiles("bun-unlink", { a: "Hello, world!" }); const file = Bun.file(path.join(dir, "a")); - expect(file.unlink()).resolves.toBeUndefined(); + await expect(file.unlink()).resolves.toBeUndefined(); expect(await Bun.file(path.join(dir, "a")).exists()).toBe(false); }); test("Bun.file(path).delete() does not throw", async () => { const dir = tempDirWithFiles("bun-unlink", { a: "Hello, world!" }); const file = Bun.file(path.join(dir, "a")); - expect(file.delete()).resolves.toBeUndefined(); + await expect(file.delete()).resolves.toBeUndefined(); expect(await Bun.file(path.join(dir, "a")).exists()).toBe(false); }); diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index b5bbca34e6e..c52cbaf59dd 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -85,7 +85,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(13); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("Hello, World!"); + await expect(blob.text()).resolves.toBe("Hello, World!"); }); it("percent encoded (invalid)", async () => { var url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3"; @@ -103,7 +103,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(13); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("Hello, World!"); + await expect(blob.text()).resolves.toBe("Hello, World!"); url = "data:,helloworld!"; res = await fetch(url); @@ -114,7 +114,7 @@ describe("fetch data urls", () => { blob = await res.blob(); expect(blob.size).toBe(11); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("helloworld!"); + await expect(blob.text()).resolves.toBe("helloworld!"); }); it("unstrict parsing of invalid URL characters", async () => { var url = "data:application/json,{%7B%7D}"; @@ -126,7 +126,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(4); expect(blob.type).toBe("application/json;charset=utf-8"); - expect(blob.text()).resolves.toBe("{{}}"); + await expect(blob.text()).resolves.toBe("{{}}"); }); it("unstrict parsing of double percent characters", async () => { var url = "data:application/json,{%%7B%7D%%}%%"; @@ -138,7 +138,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(9); expect(blob.type).toBe("application/json;charset=utf-8"); - expect(blob.text()).resolves.toBe("{%{}%%}%%"); + await expect(blob.text()).resolves.toBe("{%{}%%}%%"); }); it("data url (invalid)", async () => { var url = "data:Hello%2C%20World!"; @@ -157,7 +157,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(4); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("😀"); + await expect(blob.text()).resolves.toBe("😀"); }); it("should work with Request", async () => { var req = new Request("data:,Hello%2C%20World!"); @@ -169,7 +169,7 @@ describe("fetch data urls", () => { var blob = await res.blob(); expect(blob.size).toBe(13); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("Hello, World!"); + await expect(blob.text()).resolves.toBe("Hello, World!"); req = new Request("data:,😀"); res = await fetch(req); @@ -180,7 +180,7 @@ describe("fetch data urls", () => { blob = await res.blob(); expect(blob.size).toBe(4); expect(blob.type).toBe("text/plain;charset=utf-8"); - expect(blob.text()).resolves.toBe("😀"); + await expect(blob.text()).resolves.toBe("😀"); }); it("should work with Request (invalid)", async () => { var req = new Request("data:Hello%2C%20World!"); @@ -2135,13 +2135,13 @@ describe("http/1.1 response body length", () => { it("should read text until socket closed", async () => { const response = await fetch(`http://${getHost()}/text`); expect(response.status).toBe(200); - expect(response.text()).resolves.toBe("Hello, World!"); + await expect(response.text()).resolves.toBe("Hello, World!"); }); it("should read json until socket closed", async () => { const response = await fetch(`http://${getHost()}/json`); expect(response.status).toBe(200); - expect(response.json()).resolves.toEqual({ "hello": "World" }); + await expect(response.json()).resolves.toEqual({ "hello": "World" }); }); it("should disable keep-alive", async () => { @@ -2151,38 +2151,38 @@ describe("http/1.1 response body length", () => { // the 1st http response body + the full 2nd http response as text const response = await fetch(`http://${getHost()}/keepalive/bad`); expect(response.status).toBe(200); - expect(response.text()).resolves.toHaveLength(95); + await expect(response.text()).resolves.toHaveLength(95); }); }); it("should support keep-alive", async () => { const response = await fetch(`http://${getHost()}/keepalive`); expect(response.status).toBe(200); - expect(response.text()).resolves.toBe("Hello, World!"); + await expect(response.text()).resolves.toBe("Hello, World!"); }); it("should support transfer-encoding: chunked", async () => { const response = await fetch(`http://${getHost()}/chunked`); expect(response.status).toBe(200); - expect(response.text()).resolves.toBe("Hello, World!"); + await expect(response.text()).resolves.toBe("Hello, World!"); }); it("should support non-zero content-length", async () => { const response = await fetch(`http://${getHost()}/non-empty`); expect(response.status).toBe(200); - expect(response.text()).resolves.toBe("Hello, World!"); + await expect(response.text()).resolves.toBe("Hello, World!"); }); it("should support content-length: 0", async () => { const response = await fetch(`http://${getHost()}/empty`); expect(response.status).toBe(200); - expect(response.arrayBuffer()).resolves.toHaveLength(0); + await expect(response.arrayBuffer()).resolves.toHaveLength(0); }); it.todoIf(isBroken)("should ignore body on HEAD", async () => { const response = await fetch(`http://${getHost()}/text`, { method: "HEAD" }); expect(response.status).toBe(200); - expect(response.arrayBuffer()).resolves.toHaveLength(0); + await expect(response.arrayBuffer()).resolves.toHaveLength(0); }); }); describe("fetch Response life cycle", () => { diff --git a/test/js/web/fetch/form-data-boundary-crash.test.ts b/test/js/web/fetch/form-data-boundary-crash.test.ts index 7f7230ef0cc..8caa8950066 100644 --- a/test/js/web/fetch/form-data-boundary-crash.test.ts +++ b/test/js/web/fetch/form-data-boundary-crash.test.ts @@ -8,7 +8,7 @@ test('Response.formData() rejects on boundary=" (lone double-quote)', async () = const response = new Response("body", { headers: { "content-type": 'multipart/form-data; boundary="' }, }); - expect(response.formData()).rejects.toThrow(); + await expect(response.formData()).rejects.toThrow(); }); test('Request.formData() rejects on boundary=" (lone double-quote)', async () => { @@ -17,24 +17,24 @@ test('Request.formData() rejects on boundary=" (lone double-quote)', async () => body: "body", headers: { "content-type": 'multipart/form-data; boundary="' }, }); - expect(request.formData()).rejects.toThrow(); + await expect(request.formData()).rejects.toThrow(); }); test('Blob.formData() rejects on boundary=" (lone double-quote)', async () => { const blob = new Blob(["body"], { type: 'multipart/form-data; boundary="' }); - expect(blob.formData()).rejects.toThrow(); + await expect(blob.formData()).rejects.toThrow(); }); test('Response.formData() rejects on boundary="abc (unclosed double-quote)', async () => { const response = new Response("body", { headers: { "content-type": 'multipart/form-data; boundary="abc' }, }); - expect(response.formData()).rejects.toThrow(); + await expect(response.formData()).rejects.toThrow(); }); test('Response.formData() rejects on boundary="; (lone double-quote before semicolon)', async () => { const response = new Response("body", { headers: { "content-type": 'multipart/form-data; boundary="; charset=utf-8' }, }); - expect(response.formData()).rejects.toThrow(); + await expect(response.formData()).rejects.toThrow(); }); diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 5256b7502f1..24bae7bb270 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -787,13 +787,13 @@ it("ReadableStream rejects pending reads when the lock is released", async () => let read = reader.read(); reader.releaseLock(); - expect(read).rejects.toThrow( + await expect(read).rejects.toThrow( expect.objectContaining({ name: "AbortError", code: "ERR_STREAM_RELEASE_LOCK", }), ); - expect(reader.closed).rejects.toThrow( + await expect(reader.closed).rejects.toThrow( expect.objectContaining({ name: "AbortError", code: "ERR_STREAM_RELEASE_LOCK", diff --git a/test/regression/issue/06443.test.ts b/test/regression/issue/06443.test.ts index 418d8636974..4c218849060 100644 --- a/test/regression/issue/06443.test.ts +++ b/test/regression/issue/06443.test.ts @@ -31,7 +31,7 @@ describe("Bun.serve()", () => { const proto = options.tls ? "https" : "http"; const target = `${proto}://localhost:${server.port}/`; const response = await fetch(target, { tls: { rejectUnauthorized: false } }); - expect(response.text()).resolves.toMatch(url); + await expect(response.text()).resolves.toMatch(url); } finally { server.stop(true); } From 04311d9425182ba6b354814bc5e2d5e50577c366 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 04:11:33 +0000 Subject: [PATCH 12/26] Await .rejects in deinitialization fixture test/bake/fixtures/deinitialization/test.ts is run as a subprocess by deinitialization.test.ts and has two un-awaited `expect(fetch(...)).rejects.toThrow()` calls. The first triggers the plugin callback that stops the server; without blocking, control reached the second fetch before the server was actually down. Missed in the earlier sweep because the grep only covered *.test.{ts,js} files. Fixes the all-platform deinitialization.test.ts failure from build 53957. --- test/bake/fixtures/deinitialization/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bake/fixtures/deinitialization/test.ts b/test/bake/fixtures/deinitialization/test.ts index b6b41533a15..506769990fa 100644 --- a/test/bake/fixtures/deinitialization/test.ts +++ b/test/bake/fixtures/deinitialization/test.ts @@ -58,7 +58,7 @@ async function run({ closeActiveConnections = false, sendAnyRequests = true, web if (sendAnyRequests) { if (closeActiveConnections) { - expect(fetch(server.url.origin, { keepalive: false })).rejects.toThrow("closed unexpectedly"); + await expect(fetch(server.url.origin, { keepalive: false })).rejects.toThrow("closed unexpectedly"); } else { const response = await fetch(server.url.origin, { keepalive: false }); expect(response.status).toBe(200); @@ -68,7 +68,7 @@ async function run({ closeActiveConnections = false, sendAnyRequests = true, web } // Server is closed - expect(fetch(server.url.origin, { keepalive: false })).rejects.toThrow("Unable to connect"); + await expect(fetch(server.url.origin, { keepalive: false })).rejects.toThrow("Unable to connect"); } await main(); From 3ea011e1b10b620c0551504ad9a3fa6508558458 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 05:29:46 +0000 Subject: [PATCH 13/26] Save/restore is_async_rerun in PendingMatcher.rerun Completes the symmetry with flags and async_rerun_srcloc. A sibling PendingMatcher on the same input promise can rerun inside matcher_fn.call() when the matcher re-enters the event loop (toThrow's getValueAsToThrow -> waitForPromise -> tick drains microtasks); the inner defer must not clobber the outer rerun's is_async_rerun flag, or inlineSnapshot() in the outer would fall back to getCallerSrcLoc() on a frameless stack. --- src/runtime/test_runner/expect.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index c54ac584ac8..2b05e41a6aa 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -348,9 +348,18 @@ pub const Expect = struct { // captured location for the duration of the call and restored // afterwards so two deferred matchers on the same `expect()` // each see their own call site in `inlineSnapshot()`. + // + // All three are saved/restored (not hard-reset) because a + // sibling `PendingMatcher` on the same input promise can rerun + // *inside* `matcher_fn.call()` when the matcher re-enters the + // event loop (e.g. `toThrow` → `getValueAsToThrow` → + // `waitForPromise` → `tick` drains the microtask queue), and + // the inner defer must not clobber the outer's state. + var saved_is_rerun: bool = false; var saved_flags: Expect.Flags = undefined; var saved_srcloc: ?CallFrame.CallerSrcLoc = null; if (Expect.fromJS(expect_this)) |expect| { + saved_is_rerun = expect.is_async_rerun; expect.is_async_rerun = true; expect.counted_expect_call = this.was_counted_before_defer; saved_flags = expect.flags; @@ -359,7 +368,7 @@ pub const Expect = struct { expect.async_rerun_srcloc = this.srcloc_at_defer; } defer if (Expect.fromJS(expect_this)) |expect| { - expect.is_async_rerun = false; + expect.is_async_rerun = saved_is_rerun; expect.flags = saved_flags; expect.async_rerun_srcloc = saved_srcloc; }; From ee142818f56fae35841a226f8c8c449dfdd31b10 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 22:18:13 +0000 Subject: [PATCH 14/26] Port PendingMatcher to Rust after #30412 rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main's #30412 rewrote expect.zig as expect.rs (and each expect/*.zig as *.rs). The .zig files under src/runtime/test_runner/ are no longer compiled — the live implementation is Rust. The rebase applied the earlier Zig commits cleanly (git followed the src/test_runner/ → src/runtime/test_runner/ rename), but they landed on dead code. This ports the deferred-matcher mechanism to Rust and reverts the dead Zig changes: expect.rs: - Expect gains is_async_rerun, counted_expect_call, async_rerun_srcloc (Cell<_> for interior mutability). - get_value() now takes &CallFrame and returns MaybeDeferred; when the captured promise is still pending, it creates a PendingMatcher (Strong refs to the Expect wrapper, matcher fn, and args array; a JSPromiseStrong for the deferred result; and per-defer snapshots of counted_expect_call / flags / caller srcloc), attaches it via JSValue::then, and returns the deferred promise. - matcher_prelude() / mock_prologue() / the shared run_* helpers propagate the deferred case. - increment_expect_call_counter() is gated on counted_expect_call so a deferred rerun doesn't double-count; post_match() resets it. - inline_snapshot() uses the PendingMatcher's captured srcloc during a rerun (the user's frame is gone from the stack). - Bun__Expect__PendingMatcher__onResolve/onReject are exported via jsc_host_abi!{ #[no_mangle] } so promiseHandlerID() can match them by fn-ptr identity. mod.rs: ready_matcher!/ready_value!/ready_mock! macros for the early-return-on-Deferred pattern at each call site (defined at module top-level, outside cfg_jsc!, so they're addressable via crate:: paths). CallFrame.rs: CallerSrcLoc is now Clone+Copy (needed for Cell>). jsc/lib.rs: re-export CallerSrcLoc. 35 expect/*.rs matcher files: mechanical update to pass the callframe through and early-return the deferred promise via the ready_* macros. Test: relaxed the MAX_INFLIGHT check from ==10 to >1 — the Rust runner caps test.concurrent at 5 on ASAN builds (20 otherwise); the point is >1 proves .resolves no longer serializes. --- src/jsc/CallFrame.rs | 1 + src/jsc/lib.rs | 2 +- src/runtime/test_runner/expect.rs | 359 +++++++++++++++++- src/runtime/test_runner/expect.zig | 257 +------------ src/runtime/test_runner/expect/toBe.rs | 3 +- src/runtime/test_runner/expect/toBe.zig | 2 +- src/runtime/test_runner/expect/toBeArray.zig | 2 +- .../test_runner/expect/toBeArrayOfSize.rs | 2 +- .../test_runner/expect/toBeArrayOfSize.zig | 2 +- .../test_runner/expect/toBeBoolean.zig | 2 +- src/runtime/test_runner/expect/toBeCloseTo.rs | 2 +- .../test_runner/expect/toBeCloseTo.zig | 2 +- src/runtime/test_runner/expect/toBeDate.zig | 2 +- .../test_runner/expect/toBeDefined.zig | 2 +- src/runtime/test_runner/expect/toBeEmpty.rs | 2 +- src/runtime/test_runner/expect/toBeEmpty.zig | 2 +- .../test_runner/expect/toBeEmptyObject.rs | 2 +- .../test_runner/expect/toBeEmptyObject.zig | 2 +- src/runtime/test_runner/expect/toBeEven.zig | 2 +- src/runtime/test_runner/expect/toBeFalse.zig | 2 +- src/runtime/test_runner/expect/toBeFalsy.zig | 2 +- src/runtime/test_runner/expect/toBeFinite.zig | 2 +- .../test_runner/expect/toBeFunction.zig | 2 +- .../test_runner/expect/toBeGreaterThan.zig | 2 +- .../expect/toBeGreaterThanOrEqual.zig | 2 +- .../test_runner/expect/toBeInstanceOf.rs | 2 +- .../test_runner/expect/toBeInstanceOf.zig | 2 +- .../test_runner/expect/toBeInteger.zig | 2 +- .../test_runner/expect/toBeLessThan.zig | 2 +- .../expect/toBeLessThanOrEqual.zig | 2 +- src/runtime/test_runner/expect/toBeNaN.zig | 2 +- .../test_runner/expect/toBeNegative.zig | 2 +- src/runtime/test_runner/expect/toBeNil.zig | 2 +- src/runtime/test_runner/expect/toBeNull.zig | 2 +- src/runtime/test_runner/expect/toBeNumber.zig | 2 +- src/runtime/test_runner/expect/toBeObject.rs | 2 +- src/runtime/test_runner/expect/toBeObject.zig | 2 +- src/runtime/test_runner/expect/toBeOdd.zig | 2 +- src/runtime/test_runner/expect/toBeOneOf.rs | 3 +- src/runtime/test_runner/expect/toBeOneOf.zig | 2 +- .../test_runner/expect/toBePositive.zig | 2 +- src/runtime/test_runner/expect/toBeString.zig | 2 +- src/runtime/test_runner/expect/toBeSymbol.zig | 2 +- src/runtime/test_runner/expect/toBeTrue.zig | 2 +- src/runtime/test_runner/expect/toBeTruthy.zig | 2 +- src/runtime/test_runner/expect/toBeTypeOf.rs | 2 +- src/runtime/test_runner/expect/toBeTypeOf.zig | 2 +- .../test_runner/expect/toBeUndefined.zig | 2 +- .../test_runner/expect/toBeValidDate.rs | 2 +- .../test_runner/expect/toBeValidDate.zig | 2 +- src/runtime/test_runner/expect/toBeWithin.rs | 5 +- src/runtime/test_runner/expect/toBeWithin.zig | 2 +- src/runtime/test_runner/expect/toContain.rs | 3 +- src/runtime/test_runner/expect/toContain.zig | 2 +- .../test_runner/expect/toContainAllKeys.zig | 2 +- .../test_runner/expect/toContainAllValues.zig | 2 +- .../test_runner/expect/toContainAnyKeys.zig | 2 +- .../test_runner/expect/toContainAnyValues.zig | 2 +- .../test_runner/expect/toContainEqual.rs | 3 +- .../test_runner/expect/toContainEqual.zig | 2 +- .../test_runner/expect/toContainKey.zig | 2 +- .../test_runner/expect/toContainKeys.zig | 2 +- .../test_runner/expect/toContainValue.zig | 2 +- .../test_runner/expect/toContainValues.zig | 2 +- src/runtime/test_runner/expect/toEndWith.zig | 2 +- src/runtime/test_runner/expect/toEqual.rs | 3 +- src/runtime/test_runner/expect/toEqual.zig | 2 +- .../expect/toEqualIgnoringWhitespace.rs | 3 +- .../expect/toEqualIgnoringWhitespace.zig | 2 +- .../test_runner/expect/toHaveBeenCalled.rs | 3 +- .../test_runner/expect/toHaveBeenCalled.zig | 2 +- .../expect/toHaveBeenCalledOnce.rs | 10 +- .../expect/toHaveBeenCalledOnce.zig | 2 +- .../expect/toHaveBeenCalledTimes.rs | 10 +- .../expect/toHaveBeenCalledTimes.zig | 2 +- .../expect/toHaveBeenCalledWith.rs | 10 +- .../expect/toHaveBeenCalledWith.zig | 2 +- .../expect/toHaveBeenLastCalledWith.rs | 10 +- .../expect/toHaveBeenLastCalledWith.zig | 2 +- .../expect/toHaveBeenNthCalledWith.rs | 10 +- .../expect/toHaveBeenNthCalledWith.zig | 2 +- .../expect/toHaveLastReturnedWith.rs | 10 +- .../expect/toHaveLastReturnedWith.zig | 2 +- .../test_runner/expect/toHaveLength.rs | 3 +- .../test_runner/expect/toHaveLength.zig | 2 +- .../expect/toHaveNthReturnedWith.rs | 10 +- .../expect/toHaveNthReturnedWith.zig | 2 +- .../test_runner/expect/toHaveProperty.rs | 5 +- .../test_runner/expect/toHaveProperty.zig | 2 +- .../test_runner/expect/toHaveReturned.rs | 6 +- .../test_runner/expect/toHaveReturned.zig | 2 +- .../test_runner/expect/toHaveReturnedWith.rs | 10 +- .../test_runner/expect/toHaveReturnedWith.zig | 2 +- src/runtime/test_runner/expect/toInclude.zig | 2 +- src/runtime/test_runner/expect/toMatch.rs | 3 +- src/runtime/test_runner/expect/toMatch.zig | 2 +- .../expect/toMatchInlineSnapshot.rs | 5 +- .../expect/toMatchInlineSnapshot.zig | 2 +- .../test_runner/expect/toMatchObject.rs | 3 +- .../test_runner/expect/toMatchObject.zig | 2 +- .../test_runner/expect/toMatchSnapshot.rs | 5 +- .../test_runner/expect/toMatchSnapshot.zig | 2 +- .../test_runner/expect/toStartWith.zig | 2 +- .../test_runner/expect/toStrictEqual.rs | 3 +- .../test_runner/expect/toStrictEqual.zig | 2 +- src/runtime/test_runner/expect/toThrow.rs | 2 +- src/runtime/test_runner/expect/toThrow.zig | 3 +- .../toThrowErrorMatchingInlineSnapshot.rs | 5 +- .../toThrowErrorMatchingInlineSnapshot.zig | 3 +- .../expect/toThrowErrorMatchingSnapshot.rs | 13 +- .../expect/toThrowErrorMatchingSnapshot.zig | 3 +- src/runtime/test_runner/mod.rs | 41 +- .../bun/test/expect-resolves-pending.test.ts | 6 +- 113 files changed, 540 insertions(+), 448 deletions(-) diff --git a/src/jsc/CallFrame.rs b/src/jsc/CallFrame.rs index faf1707c51b..0609f55fe7e 100644 --- a/src/jsc/CallFrame.rs +++ b/src/jsc/CallFrame.rs @@ -259,6 +259,7 @@ impl Arguments { } } +#[derive(Clone, Copy)] pub struct CallerSrcLoc { pub str: bun_core::String, pub line: c_uint, diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index e1e607847f0..f63effb3a23 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1381,7 +1381,7 @@ pub use self::js_object::{ExternColumnIdentifier, ExternColumnIdentifierValue, J // ────────────────────────────────────────────────────────────────────────── #[path = "CallFrame.rs"] pub mod call_frame; -pub use self::call_frame::{ArgumentsSlice, CallFrame}; +pub use self::call_frame::{ArgumentsSlice, CallFrame, CallerSrcLoc}; /// Lives here (not in `bun_sys_jsc`) because the orphan /// rule requires either the trait or the type to be local; `FromJsEnum` is. diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bcfd3b587de..b4e476e2d9a 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -40,6 +40,40 @@ pub struct Expect { pub flags: Cell, pub parent: Option, pub custom_label: bun_core::String, + /// Set to true while re-invoking a matcher from a `.resolves`/`.rejects` + /// `.then()` callback so we don't try to defer again. + pub is_async_rerun: Cell, + /// Set by `increment_expect_call_counter()` so a deferred re-run doesn't + /// count the same expectation twice. Reset by `post_match()` so multiple + /// matchers on the same `expect()` each count; `PendingMatcher` snapshots + /// it at defer time and restores it before the re-run. + pub counted_expect_call: Cell, + /// The caller's source location for the currently-executing deferred + /// re-run, borrowed from `PendingMatcher.srcloc_at_defer`. Written by + /// `PendingMatcher::rerun()` for the duration of the re-invoked matcher + /// call and restored afterwards. `inline_snapshot()` reads it (gated on + /// `is_async_rerun`) because the user's frame is no longer on the stack. + /// Owned by the `PendingMatcher`, not by `Expect`. + pub async_rerun_srcloc: Cell>, +} + +/// Result of `Expect::get_value()`: either the (promise-unwrapped) value the +/// matcher should operate on, or a deferred promise that the matcher must +/// return to its caller because `.resolves`/`.rejects` was used on a +/// still-pending input promise. +pub enum MaybeDeferred { + Value(JSValue), + /// The input promise is still pending. A `PendingMatcher` has been + /// attached via `.then()` to re-invoke the matcher once it settles; the + /// matcher must return this promise to its caller now. + Deferred(JSValue), +} + +/// Result of `Expect::matcher_prelude()`. +pub enum MatcherStart<'a> { + Ready(PostMatchGuard<'a>, JSValue, bool), + /// See [`MaybeDeferred::Deferred`]. + Deferred(JSValue), } @@ -192,6 +226,10 @@ impl Expect { } pub fn increment_expect_call_counter(&self) { + if self.counted_expect_call.get() { + return; + } + self.counted_expect_call.set(true); let Some(parent) = self.parent.as_ref() else { return }; // not in bun:test let Some(buntest_strong) = parent.bun_test() else { return }; // the test file this expect() call was for is no longer let buntest = buntest_strong.get(); @@ -357,15 +395,23 @@ impl Expect { Ok(this_value) } + /// Retrieves the captured value passed to `expect(...)`, processing + /// `.resolves`/`.rejects` if set. + /// + /// Returns `MaybeDeferred::Deferred` when the captured value is a + /// still-pending promise. In that case the matcher is deferred: a + /// `.then()` is attached that re-invokes the matcher once the promise + /// settles, and the matcher must immediately return the deferred promise. pub fn get_value( &self, global_this: &JSGlobalObject, this_value: JSValue, + callframe: &CallFrame, // Every caller passes a string literal, so accept `&str` // (BStr::new below takes `AsRef<[u8]>`, so no copy). matcher_name: &str, matcher_params_fmt: &'static str, - ) -> JsResult { + ) -> JsResult { let Some(value) = super::expect::js::captured_value_get_cached(this_value) else { return Err(global_this.throw2( "Internal error: the expect(value) was garbage collected but it should not have been!", @@ -374,6 +420,10 @@ impl Expect { }; value.ensure_still_alive(); + if let Some(deferred) = self.maybe_defer_matcher(global_this, this_value, callframe, value)? { + return Ok(MaybeDeferred::Deferred(deferred)); + } + #[allow(clippy::disallowed_methods)] // template is a runtime parameter let matcher_params = Output::pretty_fmt_rt(matcher_params_fmt, Output::enable_ansi_colors_stderr()); Self::process_promise( @@ -385,6 +435,56 @@ impl Expect { matcher_params, false, ) + .map(MaybeDeferred::Value) + } + + /// Returns the promise that a deferred matcher should return to its + /// caller. Only valid when `get_value()` returned + /// `MaybeDeferred::Deferred`. + pub fn deferred_result(&self, this_value: JSValue) -> JSValue { + super::expect::js::result_value_get_cached(this_value).unwrap_or(JSValue::UNDEFINED) + } + + /// If `.resolves`/`.rejects` is set and `value` is a still-pending + /// promise, sets up a `.then()` callback to re-invoke the current matcher + /// once the promise settles, stores the returned-to-caller promise in the + /// `resultValue` slot, and returns it. Returns `None` otherwise. + /// + /// This replaces the old synchronous `wait_for_promise()` which would + /// hang forever if the promise could only be resolved by JavaScript + /// sitting above the matcher on the call stack (#14950). + fn maybe_defer_matcher( + &self, + global_this: &JSGlobalObject, + this_value: JSValue, + callframe: &CallFrame, + value: JSValue, + ) -> JsResult> { + if self.flags.get().promise() == Promise::None { + return Ok(None); + } + if self.is_async_rerun.get() { + return Ok(None); + } + let Some(promise) = value.as_any_promise() else { + return Ok(None); + }; + if promise.status() != js_promise::Status::Pending { + return Ok(None); + } + + promise.set_handled(global_this.vm()); + + let deferred = PendingMatcher::create( + global_this, + this_value, + callframe, + value, + self.counted_expect_call.get(), + self.flags.get(), + )?; + super::expect::js::result_value_set_cached(this_value, global_this, deferred); + Ok(Some(deferred)) } /// Processes the async flags (resolves/rejects), waiting for the async value if needed. @@ -651,6 +751,9 @@ impl Expect { flags: Cell::new(Flags::default()), custom_label, parent: active_execution_entry_ref, + is_async_rerun: Cell::new(false), + counted_expect_call: Cell::new(false), + async_rerun_srcloc: Cell::new(None), }; // `JsClass::to_js` boxes `self` and hands the pointer to `${T}__create`. let expect_js_value = expect.to_js(global_this); @@ -1075,11 +1178,19 @@ impl Expect { let buntest = buntest_strong.get(); // 1. find the src loc of the snapshot - let srcloc = call_frame.get_caller_src_loc(global_this); + // When re-running from a deferred `.resolves`/`.rejects` + // `.then()` callback, the user's frame is no longer on the + // stack; `PendingMatcher::rerun()` lends its captured location + // via `async_rerun_srcloc` for the duration of the call. + let (srcloc, owns_srcloc) = match (this.is_async_rerun.get(), this.async_rerun_srcloc.get()) { + (true, Some(loc)) => (loc, false), + _ => (call_frame.get_caller_src_loc(global_this), true), + }; // bun_core::String is Copy // with no Drop, so wrap in the RAII guard to release the +1 on - // every exit path (including the early returns below). - let _srcloc_str_guard = bun_core::OwnedString::new(srcloc.str); + // every exit path (including the early returns below). When the + // srcloc is borrowed from `PendingMatcher`, it owns the deref. + let _srcloc_str_guard = owns_srcloc.then(|| bun_core::OwnedString::new(srcloc.str)); let file_id = buntest.file_id; // MultiArrayList::get requires MultiArrayElement (derive pending); // use the column accessor which already compiles in jest.rs. @@ -1560,6 +1671,7 @@ impl Expect { // thenable's user-defined `then`), which can call another matcher on this same // `expect()` chain; aliased `&Expect` is sound, aliased `&mut Expect` is not. let expect = unsafe { &*expect_ptr }; + let expect = expect.post_match_guard(global_this); // if we got an Expect instance, then it's a non-static call (`expect().myMatcher`), // so now execute the symmetric matching @@ -1579,6 +1691,9 @@ impl Expect { "Internal consistency error: failed to retrieve the captured value" ))); }; + if let Some(deferred) = expect.maybe_defer_matcher(global_this, this_value, call_frame, value)? { + return Ok(deferred); + } value = Self::process_promise( expect.custom_label.clone(), expect.flags.get(), @@ -1709,6 +1824,10 @@ impl Expect { } pub fn post_match(&self, global_this: &JSGlobalObject) { + // Reset so the *next* matcher on this `expect()` instance counts + // independently. `PendingMatcher` snapshots the pre-reset value so + // a deferred rerun observes the state at the point of defer. + self.counted_expect_call.set(false); global_this.bun_vm().auto_garbage_collect(); } @@ -1737,14 +1856,18 @@ impl Expect { &'a self, global: &'a JSGlobalObject, this_value: JSValue, + callframe: &CallFrame, matcher_name: &str, matcher_params: &'static str, - ) -> JsResult<(PostMatchGuard<'a>, JSValue, bool)> { + ) -> JsResult> { let this = self.post_match_guard(global); - let value = this.get_value(global, this_value, matcher_name, matcher_params)?; + let value = match this.get_value(global, this_value, callframe, matcher_name, matcher_params)? { + MaybeDeferred::Value(v) => v, + MaybeDeferred::Deferred(p) => return Ok(MatcherStart::Deferred(p)), + }; this.increment_expect_call_counter(); let not = this.flags.get().not(); - Ok((this, value, not)) + Ok(MatcherStart::Ready(this, value, not)) } // extern shim emitted by `#[bun_jsc::JsClass]` codegen (TypeClass__construct/__call); bare `#[host_fn]` cannot target an associated fn without a receiver. @@ -1791,6 +1914,204 @@ impl Drop for PostMatchGuard<'_> { } } +/// State for a `.resolves`/`.rejects` matcher call whose promise hasn't +/// settled yet. When it does, we re-invoke the matcher and resolve/reject +/// `deferred` with the outcome. +pub struct PendingMatcher { + /// The `Expect` JS instance (also keeps capturedValue alive). + expect_this: bun_jsc::Strong, + /// The native matcher function being called (e.g. `toBe`). + matcher_fn: bun_jsc::Strong, + /// JSArray of the arguments the matcher was called with. + matcher_args: bun_jsc::Strong, + /// The promise returned to the caller of the matcher. + deferred: bun_jsc::JSPromiseStrong, + /// Whether `increment_expect_call_counter()` had already run by the + /// time we deferred. Restored on re-run so the counter is bumped + /// exactly once regardless of whether this matcher calls it before or + /// after `get_value()`. + was_counted_before_defer: bool, + /// The `Expect.flags` at the time we deferred. `.not` / `.resolves` / + /// `.rejects` mutate flags in place on the shared instance, so a later + /// matcher call on the same `expect()` could flip them before this + /// re-run fires; restore so the re-run observes the flags the user + /// wrote for *this* matcher call. + flags_at_defer: Flags, + /// The caller's source location at the time we deferred, captured + /// while the user's frame is still on the stack. `inline_snapshot()` + /// needs it on the re-run to know which line to write the snapshot + /// back to. Stored per-PendingMatcher (not on the shared `Expect`) so + /// two deferred inline-snapshot matchers on the same `expect()` + /// instance each write to their own call site. + srcloc_at_defer: bun_jsc::CallerSrcLoc, +} + +impl PendingMatcher { + fn create( + global_this: &JSGlobalObject, + this_value: JSValue, + callframe: &CallFrame, + promise_value: JSValue, + was_counted: bool, + flags: Flags, + ) -> JsResult { + let args = callframe.arguments(); + let args_array = JSValue::create_empty_array(global_this, args.len())?; + for (i, arg) in args.iter().enumerate() { + args_array.put_index(global_this, i as u32, *arg)?; + } + + let pending = Box::new(PendingMatcher { + expect_this: bun_jsc::Strong::create(this_value, global_this), + matcher_fn: bun_jsc::Strong::create(callframe.callee(), global_this), + matcher_args: bun_jsc::Strong::create(args_array, global_this), + deferred: bun_jsc::JSPromiseStrong::init(global_this), + was_counted_before_defer: was_counted, + flags_at_defer: flags, + srcloc_at_defer: callframe.get_caller_src_loc(global_this), + }); + let deferred_value = pending.deferred.value(); + + promise_value.then( + global_this, + Box::into_raw(pending), + Bun__Expect__PendingMatcher__onResolve, + Bun__Expect__PendingMatcher__onReject, + ); + + Ok(deferred_value) + } + + fn on_settle(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + let args = callframe.arguments_old::<2>(); + let ctx = args.ptr[1]; + if ctx.is_empty_or_undefined_or_null() { + return Ok(JSValue::UNDEFINED); + } + // SAFETY: `ctx` was produced by `Box::into_raw` in `create()` and is + // consumed exactly once here (onResolve and onReject are mutually + // exclusive for a given promise settlement). + let mut this = + unsafe { Box::from_raw(ctx.as_ptr_address() as *mut PendingMatcher) }; + this.settle(global_this); + // `Box` drop releases all `Strong` refs and the srcloc string. + Ok(JSValue::UNDEFINED) + } + + fn settle(&mut self, global_this: &JSGlobalObject) { + match self.rerun(global_this) { + Ok(result) => { + let _ = self.deferred.resolve(global_this, result); + } + Err(_) => { + let exception = global_this + .try_take_exception() + .unwrap_or(JSValue::UNDEFINED); + let _ = self.deferred.reject(global_this, Ok(exception)); + } + } + } + + fn rerun(&mut self, global_this: &JSGlobalObject) -> JsResult { + let expect_this = self.expect_this.get(); + let matcher_fn = self.matcher_fn.get(); + let args_array = self.matcher_args.get(); + + // Extract the original arguments back out of the array. Most + // built-in matchers take 0-2 arguments, but `toHaveBeenCalledWith` + // and `expect.extend()` custom matchers are variadic. + // PERF(port): was stack-fallback allocator — profile in Phase B + let args_len = args_array.get_length(global_this)? as usize; + let mut args_buf: Vec = Vec::with_capacity(args_len); + for i in 0..args_len { + args_buf.push(args_array.get_index(global_this, i as u32)?); + } + + // Mark the re-run so we don't try to defer a second time. The + // captured promise is now settled, so `process_promise` will extract + // its result synchronously. + // + // `is_async_rerun`, `flags`, and `async_rerun_srcloc` are + // saved/restored (not hard-reset) because a sibling `PendingMatcher` + // on the same input promise can rerun *inside* `matcher_fn.call()` + // when the matcher re-enters the event loop (e.g. `toThrow` → + // `get_value_as_to_throw` → `wait_for_promise` → `tick` drains the + // microtask queue), and the inner restore must not clobber the + // outer's state. `counted_expect_call` is restored to its value at + // the point of defer so the counter is bumped exactly once. + struct RerunGuard { + expect: *mut Expect, + saved_is_rerun: bool, + saved_flags: Flags, + saved_srcloc: Option, + } + impl Drop for RerunGuard { + fn drop(&mut self) { + // SAFETY: `expect` came from `Expect::from_js` on a value + // held live by `self.expect_this: Strong` for the duration + // of this scope. + unsafe { + (*self.expect).is_async_rerun.set(self.saved_is_rerun); + (*self.expect).flags.set(self.saved_flags); + (*self.expect).async_rerun_srcloc.set(self.saved_srcloc); + } + } + } + let _guard = Expect::from_js(expect_this).map(|expect| { + // SAFETY: `expect_this` is held live by `self.expect_this`. + let e = unsafe { &*expect }; + let guard = RerunGuard { + expect, + saved_is_rerun: e.is_async_rerun.get(), + saved_flags: e.flags.get(), + saved_srcloc: e.async_rerun_srcloc.get(), + }; + e.is_async_rerun.set(true); + e.counted_expect_call.set(self.was_counted_before_defer); + e.flags.set(self.flags_at_defer); + e.async_rerun_srcloc.set(Some(self.srcloc_at_defer)); + guard + }); + + matcher_fn.call(global_this, expect_this, &args_buf) + } +} + +impl Drop for PendingMatcher { + fn drop(&mut self) { + // Release the +1 on the captured srcloc string. + self.srcloc_at_defer.str.deref(); + // `Strong` / `JSPromiseStrong` fields release via their own Drop. + } +} + +// `ZigGlobalObject::promiseHandlerID` (C++) compares the fn-ptr passed to +// `JSValue::then` against these by identity, so the Rust thunk MUST be the +// exported symbol itself — see the PORT NOTE on `Bun__TestScope__*` in +// `bun_test.rs`. +bun_jsc::jsc_host_abi! { + #[unsafe(no_mangle)] + pub unsafe fn Bun__Expect__PendingMatcher__onResolve( + global: *mut JSGlobalObject, + frame: *mut CallFrame, + ) -> JSValue { + // SAFETY: JSC passes non-null live pointers for both. + let (global, frame) = unsafe { (&*global, &*frame) }; + bun_jsc::host_fn::to_js_host_fn_result(global, PendingMatcher::on_settle(global, frame)) + } +} +bun_jsc::jsc_host_abi! { + #[unsafe(no_mangle)] + pub unsafe fn Bun__Expect__PendingMatcher__onReject( + global: *mut JSGlobalObject, + frame: *mut CallFrame, + ) -> JSValue { + // SAFETY: JSC passes non-null live pointers for both. + let (global, frame) = unsafe { (&*global, &*frame) }; + bun_jsc::host_fn::to_js_host_fn_result(global, PendingMatcher::on_settle(global, frame)) + } +} + pub struct CustomMatcherParamsFormatter<'a> { pub colors: bool, pub global_this: &'a JSGlobalObject, @@ -2014,7 +2335,7 @@ impl Expect { matcher_name: &'static str, pred: impl FnOnce(JSValue) -> bool, ) -> JsResult { - let (this, value, not) = self.matcher_prelude(global, frame.this(), matcher_name, "")?; + let (this, value, not) = crate::ready_matcher!(self.matcher_prelude(global, frame.this(), frame, matcher_name, "")?); if pred(value) != not { return Ok(JSValue::UNDEFINED); } @@ -2065,7 +2386,7 @@ impl Expect { ))); } - let value = this.get_value(global, frame.this(), matcher_name, "expected")?; + let value = crate::ready_value!(this.get_value(global, frame.this(), frame, matcher_name, "expected")?); this.increment_expect_call_counter(); let mut pass = value.is_string(); @@ -2194,7 +2515,7 @@ impl Expect { } expected.ensure_still_alive(); - let value = this.get_value(global, this_value, matcher_name, "expected")?; + let value = crate::ready_value!(this.get_value(global, this_value, frame, matcher_name, "expected")?); if matches!(expected_array, ExpectedArray::AfterValue) && !expected.js_type().is_array() { return Err(global.throw_invalid_argument_type(matcher_name, "expected", "array")); } @@ -3056,12 +3377,15 @@ pub mod mock { pub fn mock_prologue<'a>( &'a self, global: &'a JSGlobalObject, - this_value: JSValue, + frame: &CallFrame, matcher_name: &'static str, matcher_params: &'static str, kind: MockKind, - ) -> JsResult<(PostMatchGuard<'a>, JSValue, JSValue)> { - let (this, value, _) = self.matcher_prelude(global, this_value, matcher_name, matcher_params)?; + ) -> JsResult> { + let (this, value, _) = match self.matcher_prelude(global, frame.this(), frame, matcher_name, matcher_params)? { + MatcherStart::Ready(t, v, n) => (t, v, n), + MatcherStart::Deferred(p) => return Ok(MockStart::Deferred(p)), + }; let arr = match kind { MockKind::Calls | MockKind::CallsWithSig => JSMockFunction__getCalls(global, value)?, MockKind::Returns => JSMockFunction__getReturns(global, value)?, @@ -3085,10 +3409,17 @@ pub mod mock { )), }); } - Ok((this, arr, value)) + Ok(MockStart::Ready(this, arr, value)) } } + /// Result of `Expect::mock_prologue()`. + pub enum MockStart<'a> { + Ready(PostMatchGuard<'a>, JSValue, JSValue), + /// See [`MaybeDeferred::Deferred`]. + Deferred(JSValue), + } + pub(crate) fn jest_mock_return_object_type(global_this: &JSGlobalObject, value: JSValue) -> JsResult { if let Some(type_string) = value.fast_get(global_this, bun_jsc::BuiltinName::Type)? { if type_string.is_string() { diff --git a/src/runtime/test_runner/expect.zig b/src/runtime/test_runner/expect.zig index 2b05e41a6aa..8f3cda8d855 100644 --- a/src/runtime/test_runner/expect.zig +++ b/src/runtime/test_runner/expect.zig @@ -24,23 +24,6 @@ pub const Expect = struct { flags: Flags = .{}, parent: ?*bun.jsc.Jest.bun_test.BunTest.RefData, custom_label: bun.String = bun.String.empty, - /// Set to true while re-invoking a matcher from a `.resolves`/`.rejects` - /// `.then()` callback so we don't try to defer again. - is_async_rerun: bool = false, - /// Set by `incrementExpectCallCounter()` so a deferred re-run doesn't - /// count the same expectation twice. Matchers call the counter either - /// before or after `getValue()`, so we can't rely on ordering. Reset - /// by `postMatch()` so multiple matchers on the same `expect()` each - /// count; `PendingMatcher` snapshots it at defer time and restores it - /// before the re-run. - counted_expect_call: bool = false, - /// The caller's source location for the currently-executing deferred - /// re-run, borrowed from `PendingMatcher.srcloc_at_defer`. Written by - /// `PendingMatcher.rerun()` for the duration of the re-invoked matcher - /// call and restored afterwards. `inlineSnapshot()` reads it (gated on - /// `is_async_rerun`) because the user's frame is no longer on the stack. - /// Owned by the `PendingMatcher`, not by `Expect`. - async_rerun_srcloc: ?CallFrame.CallerSrcLoc = null, pub const TestScope = struct { test_id: TestRunner.Test.ID, @@ -48,8 +31,6 @@ pub const Expect = struct { }; pub fn incrementExpectCallCounter(this: *Expect) void { - if (this.counted_expect_call) return; - this.counted_expect_call = true; const parent = this.parent orelse return; // not in bun:test var buntest_strong = parent.bunTest() orelse return; // the test file this expect() call was for is no longer defer buntest_strong.deinit(); @@ -177,220 +158,17 @@ pub const Expect = struct { return thisValue; } - /// Retrieves the captured value passed to `expect(...)`, processing - /// `.resolves`/`.rejects` if set. - /// - /// Returns `null` when the captured value is a still-pending promise. - /// In that case the matcher is deferred: a `.then()` is attached that - /// re-invokes the matcher once the promise settles, and the matcher - /// must immediately return the promise from `deferredResult()`. - pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, matcher_name: string, comptime matcher_params_fmt: string) bun.JSError!?JSValue { + pub fn getValue(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, matcher_name: string, comptime matcher_params_fmt: string) bun.JSError!JSValue { const value = js.capturedValueGetCached(thisValue) orelse { return globalThis.throw("Internal error: the expect(value) was garbage collected but it should not have been!", .{}); }; value.ensureStillAlive(); - if (try this.maybeDeferMatcher(globalThis, thisValue, callframe, value)) |_| { - return null; - } - const matcher_params = switch (Output.enable_ansi_colors_stderr) { inline else => |colors| comptime Output.prettyFmt(matcher_params_fmt, colors), }; - return try processPromise(this.custom_label, this.flags, globalThis, value, matcher_name, matcher_params, false); - } - - /// Returns the promise that a deferred matcher should return to its caller. - /// Only valid when `getValue()` returned `null`. - pub fn deferredResult(_: *Expect, thisValue: JSValue) JSValue { - return js.resultValueGetCached(thisValue) orelse .js_undefined; - } - - /// If `.resolves`/`.rejects` is set and `value` is a still-pending promise, - /// sets up a `.then()` callback to re-invoke the current matcher once the - /// promise settles, stores the returned-to-caller promise in the - /// `resultValue` slot, and returns it. Returns `null` otherwise. - /// - /// This replaces the old synchronous `waitForPromise()` which would hang - /// forever if the promise could only be resolved by JavaScript sitting - /// above the matcher on the call stack (#14950). - fn maybeDeferMatcher(this: *Expect, globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, value: JSValue) bun.JSError!?JSValue { - if (this.flags.promise == .none) return null; - if (this.is_async_rerun) return null; - const promise = value.asAnyPromise() orelse return null; - if (promise.status() != .pending) return null; - - promise.setHandled(globalThis.vm()); - - const deferred = try PendingMatcher.create(globalThis, thisValue, callframe, value, this.counted_expect_call, this.flags); - js.resultValueSetCached(thisValue, globalThis, deferred); - return deferred; - } - - /// State for a `.resolves`/`.rejects` matcher call whose promise hasn't - /// settled yet. When it does, we re-invoke the matcher and resolve/reject - /// `deferred` with the outcome. - const PendingMatcher = struct { - /// The `Expect` JS instance (also keeps capturedValue alive). - expect_this: jsc.Strong.Optional, - /// The native matcher function being called (e.g. `toBe`). - matcher_fn: jsc.Strong.Optional, - /// JSArray of the arguments the matcher was called with. - matcher_args: jsc.Strong.Optional, - /// The promise returned to the caller of the matcher. - deferred: jsc.JSPromise.Strong, - /// Whether `incrementExpectCallCounter()` had already run by the - /// time we deferred. Restored on re-run so the counter is bumped - /// exactly once regardless of whether this matcher calls it - /// before or after `getValue()`. - was_counted_before_defer: bool, - /// The `Expect.flags` at the time we deferred. `.not` / `.resolves` - /// / `.rejects` mutate flags in place on the shared instance, so a - /// later matcher call on the same `expect()` could flip them before - /// this re-run fires; restore so the re-run observes the flags the - /// user wrote for *this* matcher call. - flags_at_defer: Expect.Flags, - /// The caller's source location at the time we deferred, captured - /// while the user's frame is still on the stack. `inlineSnapshot()` - /// needs it on the re-run to know which line to write the snapshot - /// back to. Stored per-PendingMatcher (not on the shared `Expect`) - /// so two deferred inline-snapshot matchers on the same `expect()` - /// instance each write to their own call site. - srcloc_at_defer: CallFrame.CallerSrcLoc, - - fn create(globalThis: *JSGlobalObject, thisValue: JSValue, callframe: *CallFrame, promise_value: JSValue, was_counted: bool, flags: Expect.Flags) bun.JSError!JSValue { - const args = callframe.arguments(); - const args_array = try JSValue.createEmptyArray(globalThis, args.len); - for (args, 0..) |arg, i| { - try args_array.putIndex(globalThis, @intCast(i), arg); - } - - const pending = bun.new(PendingMatcher, .{ - .expect_this = .create(thisValue, globalThis), - .matcher_fn = .create(callframe.callee(), globalThis), - .matcher_args = .create(args_array, globalThis), - .deferred = jsc.JSPromise.Strong.init(globalThis), - .was_counted_before_defer = was_counted, - .flags_at_defer = flags, - .srcloc_at_defer = callframe.getCallerSrcLoc(globalThis), - }); - const deferred_value = pending.deferred.value(); - - promise_value.then(globalThis, pending, onResolve, onReject) catch { - // JSTerminated: the VM is shutting down. Clean up and return - // the (never-to-settle) promise; the caller will unwind. - pending.deinit(); - }; - - return deferred_value; - } - - pub export const Bun__Expect__PendingMatcher__onResolve = jsc.toJSHostFn(onResolve); - pub export const Bun__Expect__PendingMatcher__onReject = jsc.toJSHostFn(onReject); - - fn onResolve(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - _, const ctx = callframe.argumentsAsArray(2); - if (ctx.isEmptyOrUndefinedOrNull()) return .js_undefined; - const this: *PendingMatcher = ctx.asPromisePtr(PendingMatcher); - this.settle(globalThis); - return .js_undefined; - } - - fn onReject(globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { - _, const ctx = callframe.argumentsAsArray(2); - if (ctx.isEmptyOrUndefinedOrNull()) return .js_undefined; - const this: *PendingMatcher = ctx.asPromisePtr(PendingMatcher); - this.settle(globalThis); - return .js_undefined; - } - - fn settle(this: *PendingMatcher, globalThis: *JSGlobalObject) void { - defer this.deinit(); - - const result = this.rerun(globalThis) catch { - const exception = globalThis.tryTakeException() orelse JSValue.js_undefined; - this.deferred.reject(globalThis, exception) catch {}; - return; - }; - this.deferred.resolve(globalThis, result) catch {}; - } - - fn rerun(this: *PendingMatcher, globalThis: *JSGlobalObject) bun.JSError!JSValue { - const expect_this = this.expect_this.get() orelse return .js_undefined; - const matcher_fn = this.matcher_fn.get() orelse return .js_undefined; - const args_array = this.matcher_args.get() orelse return .js_undefined; - - // Extract the original arguments back out of the array. - // Most built-in matchers take 0-2 arguments, but - // `toHaveBeenCalledWith` and `expect.extend()` custom matchers - // are variadic, so fall back to the heap when needed. - const args_len: u32 = @intCast(try args_array.getLength(globalThis)); - var sfa = std.heap.stackFallback(inline_matcher_args * @sizeOf(JSValue), bun.default_allocator); - const allocator = sfa.get(); - const args_buf = bun.handleOom(allocator.alloc(JSValue, args_len)); - defer allocator.free(args_buf); - for (0..args_len) |i| { - args_buf[i] = try args_array.getIndex(globalThis, @intCast(i)); - } - - // Mark the re-run so we don't try to defer a second time. The - // captured promise is now settled, so `processPromise` will - // extract its result synchronously. - // - // Restore `counted_expect_call` and `flags` to their values at - // the point of defer: if the matcher incremented before - // `getValue()` on the first call, the re-run's increment is a - // no-op; if it increments after, the first call never reached - // it and the re-run does. `flags` is restored (and the current - // value put back afterwards) so a later `.not` on the same - // reused `expect()` instance doesn't leak into this re-run. - // `async_rerun_srcloc` is borrowed to this `PendingMatcher`'s - // captured location for the duration of the call and restored - // afterwards so two deferred matchers on the same `expect()` - // each see their own call site in `inlineSnapshot()`. - // - // All three are saved/restored (not hard-reset) because a - // sibling `PendingMatcher` on the same input promise can rerun - // *inside* `matcher_fn.call()` when the matcher re-enters the - // event loop (e.g. `toThrow` → `getValueAsToThrow` → - // `waitForPromise` → `tick` drains the microtask queue), and - // the inner defer must not clobber the outer's state. - var saved_is_rerun: bool = false; - var saved_flags: Expect.Flags = undefined; - var saved_srcloc: ?CallFrame.CallerSrcLoc = null; - if (Expect.fromJS(expect_this)) |expect| { - saved_is_rerun = expect.is_async_rerun; - expect.is_async_rerun = true; - expect.counted_expect_call = this.was_counted_before_defer; - saved_flags = expect.flags; - expect.flags = this.flags_at_defer; - saved_srcloc = expect.async_rerun_srcloc; - expect.async_rerun_srcloc = this.srcloc_at_defer; - } - defer if (Expect.fromJS(expect_this)) |expect| { - expect.is_async_rerun = saved_is_rerun; - expect.flags = saved_flags; - expect.async_rerun_srcloc = saved_srcloc; - }; - - return matcher_fn.call(globalThis, expect_this, args_buf); - } - - fn deinit(this: *PendingMatcher) void { - this.expect_this.deinit(); - this.matcher_fn.deinit(); - this.matcher_args.deinit(); - this.deferred.deinit(); - this.srcloc_at_defer.str.deref(); - bun.destroy(this); - } - - /// Stack-buffer size for the re-run's argument list. Most built-in - /// matchers take 0-2 positional arguments; `toHaveBeenCalledWith` - /// and custom matchers via `expect.extend()` are variadic, so - /// `settle()` heap-allocates past this. - const inline_matcher_args = 8; - }; + return processPromise(this.custom_label, this.flags, globalThis, value, matcher_name, matcher_params, false); + } /// Processes the async flags (resolves/rejects), waiting for the async value if needed. /// If no flags, returns the original value @@ -551,8 +329,6 @@ pub const Expect = struct { ) callconv(.c) void { this.custom_label.deref(); if (this.parent) |parent| parent.deref(); - // `async_rerun_srcloc` is borrowed from `PendingMatcher` for the - // duration of a re-run and is not owned here; don't deref. VirtualMachine.get().allocator.destroy(this); } @@ -983,15 +759,8 @@ pub const Expect = struct { const buntest = buntest_strong.get(); // 1. find the src loc of the snapshot - // When re-running from a deferred `.resolves`/`.rejects` - // `.then()` callback, the user's frame is no longer on the - // stack; `PendingMatcher.rerun()` lends its captured location - // via `async_rerun_srcloc` for the duration of the call. - const srcloc: CallFrame.CallerSrcLoc, const owns_srcloc: bool = if (this.is_async_rerun and this.async_rerun_srcloc != null) - .{ this.async_rerun_srcloc.?, false } - else - .{ callFrame.getCallerSrcLoc(globalThis), true }; - defer if (owns_srcloc) srcloc.str.deref(); + const srcloc = callFrame.getCallerSrcLoc(globalThis); + defer srcloc.str.deref(); const file_id = buntest.file_id; const fget = runner.files.get(file_id); @@ -1353,6 +1122,8 @@ pub const Expect = struct { /// Function that is run for either `expect.myMatcher()` call or `expect().myMatcher` call, /// and we can known which case it is based on if the `callFrame.this()` value is an instance of Expect pub fn applyCustomMatcher(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!jsc.JSValue { + defer globalThis.bunVM().autoGarbageCollect(); + // retrieve the user-provided matcher function (matcher_fn) const func: JSValue = callFrame.callee(); var matcher_fn: JSValue = getCustomMatcherFn(func, globalThis) orelse .js_undefined; @@ -1364,13 +1135,9 @@ pub const Expect = struct { // try to retrieve the Expect instance const thisValue: JSValue = callFrame.this(); const expect: *Expect = Expect.fromJS(thisValue) orelse { - defer globalThis.bunVM().autoGarbageCollect(); // if no Expect instance, assume it is a static call (`expect.myMatcher()`), so create an ExpectCustomAsymmetricMatcher instance return ExpectCustomAsymmetricMatcher.create(globalThis, callFrame, matcher_fn); }; - // Use the shared cleanup so `counted_expect_call` is reset the same - // way built-in matchers do via `defer this.postMatch(...)`. - defer expect.postMatch(globalThis); // if we got an Expect instance, then it's a non-static call (`expect().myMatcher`), // so now execute the symmetric matching @@ -1388,9 +1155,6 @@ pub const Expect = struct { var value = js.capturedValueGetCached(thisValue) orelse { return globalThis.throw("Internal consistency error: failed to retrieve the captured value", .{}); }; - if (try expect.maybeDeferMatcher(globalThis, thisValue, callFrame, value)) |deferred| { - return deferred; - } value = try processPromise(expect.custom_label, expect.flags, globalThis, value, matcher_name, matcher_params, false); value.ensureStillAlive(); @@ -1477,12 +1241,7 @@ pub const Expect = struct { return globalThis.throw("Not implemented", .{}); } - pub fn postMatch(this: *Expect, globalThis: *JSGlobalObject) void { - // Each matcher call should count independently when the user - // reuses the same `expect()` instance. `PendingMatcher` snapshots - // the flag at defer time and restores it on re-run, so clearing - // here is safe in the deferred path too. - this.counted_expect_call = false; + pub fn postMatch(_: *Expect, globalThis: *JSGlobalObject) void { var vm = globalThis.bunVM(); vm.autoGarbageCollect(); } diff --git a/src/runtime/test_runner/expect/toBe.rs b/src/runtime/test_runner/expect/toBe.rs index 3e257b363d4..f6abd5cc680 100644 --- a/src/runtime/test_runner/expect/toBe.rs +++ b/src/runtime/test_runner/expect/toBe.rs @@ -11,8 +11,7 @@ impl Expect { global_this: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let (this, left, not) = - self.matcher_prelude(global_this, callframe.this(), "toBe", "expected")?; + let (this, left, not) = crate::ready_matcher!(self.matcher_prelude(global_this, callframe.this(), callframe, "toBe", "expected")?); let arguments_ = callframe.arguments_old::<2>(); let arguments = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toBe.zig b/src/runtime/test_runner/expect/toBe.zig index 202c689ec02..0f8cba43179 100644 --- a/src/runtime/test_runner/expect/toBe.zig +++ b/src/runtime/test_runner/expect/toBe.zig @@ -16,7 +16,7 @@ pub fn toBe( this.incrementExpectCallCounter(); const right = arguments[0]; right.ensureStillAlive(); - const left = (try this.getValue(globalThis, thisValue, callframe, "toBe", "expected")) orelse return this.deferredResult(thisValue); + const left = try this.getValue(globalThis, thisValue, "toBe", "expected"); const not = this.flags.not; var pass = try right.isSameValue(left, globalThis); diff --git a/src/runtime/test_runner/expect/toBeArray.zig b/src/runtime/test_runner/expect/toBeArray.zig index e1977e5a50b..7681a13e511 100644 --- a/src/runtime/test_runner/expect/toBeArray.zig +++ b/src/runtime/test_runner/expect/toBeArray.zig @@ -2,7 +2,7 @@ pub fn toBeArray(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeArray", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeArray", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeArrayOfSize.rs b/src/runtime/test_runner/expect/toBeArrayOfSize.rs index 464e5bdd543..9b883956f16 100644 --- a/src/runtime/test_runner/expect/toBeArrayOfSize.rs +++ b/src/runtime/test_runner/expect/toBeArrayOfSize.rs @@ -21,7 +21,7 @@ pub(crate) fn to_be_array_of_size( return Err(global.throw_invalid_arguments(format_args!("toBeArrayOfSize() requires 1 argument"))); } - let value: JSValue = this.get_value(global, this_value, "toBeArrayOfSize", "")?; + let value: JSValue = crate::ready_value!(this.get_value(global, this_value, frame, "toBeArrayOfSize", "")?); let size = arguments[0]; size.ensure_still_alive(); diff --git a/src/runtime/test_runner/expect/toBeArrayOfSize.zig b/src/runtime/test_runner/expect/toBeArrayOfSize.zig index f68de18da0a..00e26612759 100644 --- a/src/runtime/test_runner/expect/toBeArrayOfSize.zig +++ b/src/runtime/test_runner/expect/toBeArrayOfSize.zig @@ -9,7 +9,7 @@ pub fn toBeArrayOfSize(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C return globalThis.throwInvalidArguments("toBeArrayOfSize() requires 1 argument", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeArrayOfSize", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeArrayOfSize", ""); const size = arguments[0]; size.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toBeBoolean.zig b/src/runtime/test_runner/expect/toBeBoolean.zig index 771aaf12121..1541a2e00dc 100644 --- a/src/runtime/test_runner/expect/toBeBoolean.zig +++ b/src/runtime/test_runner/expect/toBeBoolean.zig @@ -2,7 +2,7 @@ pub fn toBeBoolean(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeBoolean", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeBoolean", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeCloseTo.rs b/src/runtime/test_runner/expect/toBeCloseTo.rs index ba133753732..dce89c4ede7 100644 --- a/src/runtime/test_runner/expect/toBeCloseTo.rs +++ b/src/runtime/test_runner/expect/toBeCloseTo.rs @@ -40,7 +40,7 @@ impl Expect { } let received_: JSValue = - this.get_value(global, this_value, "toBeCloseTo", "expected, precision")?; + crate::ready_value!(this.get_value(global, this_value, call_frame, "toBeCloseTo", "expected, precision")?); if !received_.is_number() { return Err(global.throw_invalid_argument_type("expect", "received", "number")); } diff --git a/src/runtime/test_runner/expect/toBeCloseTo.zig b/src/runtime/test_runner/expect/toBeCloseTo.zig index f62173e853a..83708f477b4 100644 --- a/src/runtime/test_runner/expect/toBeCloseTo.zig +++ b/src/runtime/test_runner/expect/toBeCloseTo.zig @@ -26,7 +26,7 @@ pub fn toBeCloseTo(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF precision = precision_.asNumber(); } - const received_: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeCloseTo", "expected, precision")) orelse return this.deferredResult(thisValue); + const received_: JSValue = try this.getValue(globalThis, thisValue, "toBeCloseTo", "expected, precision"); if (!received_.isNumber()) { return globalThis.throwInvalidArgumentType("expect", "received", "number"); } diff --git a/src/runtime/test_runner/expect/toBeDate.zig b/src/runtime/test_runner/expect/toBeDate.zig index 83039cb4410..a1ee2a9cc40 100644 --- a/src/runtime/test_runner/expect/toBeDate.zig +++ b/src/runtime/test_runner/expect/toBeDate.zig @@ -2,7 +2,7 @@ pub fn toBeDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeDate", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDate", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeDefined.zig b/src/runtime/test_runner/expect/toBeDefined.zig index 06cb13d8945..0e3d5047086 100644 --- a/src/runtime/test_runner/expect/toBeDefined.zig +++ b/src/runtime/test_runner/expect/toBeDefined.zig @@ -2,7 +2,7 @@ pub fn toBeDefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeDefined", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeDefined", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEmpty.rs b/src/runtime/test_runner/expect/toBeEmpty.rs index aaf15206b21..e9dec3f9ebf 100644 --- a/src/runtime/test_runner/expect/toBeEmpty.rs +++ b/src/runtime/test_runner/expect/toBeEmpty.rs @@ -11,7 +11,7 @@ pub(crate) fn to_be_empty( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (_this, value, not) = this.matcher_prelude(global, frame.this(), "toBeEmpty", "")?; + let (_this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toBeEmpty", "")?); let mut pass; let mut formatter = super::make_formatter(global); // `defer formatter.deinit()` — handled by Drop. diff --git a/src/runtime/test_runner/expect/toBeEmpty.zig b/src/runtime/test_runner/expect/toBeEmpty.zig index a2dbeb1d643..b3c33d52e63 100644 --- a/src/runtime/test_runner/expect/toBeEmpty.zig +++ b/src/runtime/test_runner/expect/toBeEmpty.zig @@ -2,7 +2,7 @@ pub fn toBeEmpty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEmpty", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmpty", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEmptyObject.rs b/src/runtime/test_runner/expect/toBeEmptyObject.rs index ac1177a3f72..92bc84e2d11 100644 --- a/src/runtime/test_runner/expect/toBeEmptyObject.rs +++ b/src/runtime/test_runner/expect/toBeEmptyObject.rs @@ -9,7 +9,7 @@ pub(crate) fn to_be_empty_object( call_frame: &CallFrame, ) -> JsResult { let this_value = call_frame.this(); - let (this, value, not) = this.matcher_prelude(global, this_value, "toBeEmptyObject", "")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, this_value, call_frame, "toBeEmptyObject", "")?); let mut pass = value.is_object_empty(global)?; if not { diff --git a/src/runtime/test_runner/expect/toBeEmptyObject.zig b/src/runtime/test_runner/expect/toBeEmptyObject.zig index 26870fbfa98..4e9e09d044a 100644 --- a/src/runtime/test_runner/expect/toBeEmptyObject.zig +++ b/src/runtime/test_runner/expect/toBeEmptyObject.zig @@ -2,7 +2,7 @@ pub fn toBeEmptyObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEmptyObject", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEmptyObject", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeEven.zig b/src/runtime/test_runner/expect/toBeEven.zig index 189833cc048..55fdac875c0 100644 --- a/src/runtime/test_runner/expect/toBeEven.zig +++ b/src/runtime/test_runner/expect/toBeEven.zig @@ -3,7 +3,7 @@ pub fn toBeEven(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeEven", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeEven", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFalse.zig b/src/runtime/test_runner/expect/toBeFalse.zig index f4c876bc6ec..3e5a83bb37e 100644 --- a/src/runtime/test_runner/expect/toBeFalse.zig +++ b/src/runtime/test_runner/expect/toBeFalse.zig @@ -2,7 +2,7 @@ pub fn toBeFalse(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFalse", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalse", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFalsy.zig b/src/runtime/test_runner/expect/toBeFalsy.zig index eafd4a5deca..7e66fe6a88f 100644 --- a/src/runtime/test_runner/expect/toBeFalsy.zig +++ b/src/runtime/test_runner/expect/toBeFalsy.zig @@ -3,7 +3,7 @@ pub fn toBeFalsy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFalsy", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFalsy", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFinite.zig b/src/runtime/test_runner/expect/toBeFinite.zig index 08b44b78cdd..5e542764a98 100644 --- a/src/runtime/test_runner/expect/toBeFinite.zig +++ b/src/runtime/test_runner/expect/toBeFinite.zig @@ -2,7 +2,7 @@ pub fn toBeFinite(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFinite", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFinite", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeFunction.zig b/src/runtime/test_runner/expect/toBeFunction.zig index 8ffeb0fbd0b..5969c738540 100644 --- a/src/runtime/test_runner/expect/toBeFunction.zig +++ b/src/runtime/test_runner/expect/toBeFunction.zig @@ -2,7 +2,7 @@ pub fn toBeFunction(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeFunction", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeFunction", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeGreaterThan.zig b/src/runtime/test_runner/expect/toBeGreaterThan.zig index fdff2a0cb5a..c19a1c81bb5 100644 --- a/src/runtime/test_runner/expect/toBeGreaterThan.zig +++ b/src/runtime/test_runner/expect/toBeGreaterThan.zig @@ -14,7 +14,7 @@ pub fn toBeGreaterThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeGreaterThan", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeGreaterThan", "expected"); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig b/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig index fa4e81e2193..7051638a138 100644 --- a/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig +++ b/src/runtime/test_runner/expect/toBeGreaterThanOrEqual.zig @@ -14,7 +14,7 @@ pub fn toBeGreaterThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFr const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeGreaterThanOrEqual", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeGreaterThanOrEqual", "expected"); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeInstanceOf.rs b/src/runtime/test_runner/expect/toBeInstanceOf.rs index 02f9d2f211f..0059949770e 100644 --- a/src/runtime/test_runner/expect/toBeInstanceOf.rs +++ b/src/runtime/test_runner/expect/toBeInstanceOf.rs @@ -38,7 +38,7 @@ pub(crate) fn to_be_instance_of( expected_value.ensure_still_alive(); let value: JSValue = - this.get_value(global, this_value, "toBeInstanceOf", "expected")?; + crate::ready_value!(this.get_value(global, this_value, frame, "toBeInstanceOf", "expected")?); let not = this.flags.get().not(); let mut pass = value.is_instance_of(global, expected_value)?; diff --git a/src/runtime/test_runner/expect/toBeInstanceOf.zig b/src/runtime/test_runner/expect/toBeInstanceOf.zig index 0cf117b3e17..c0f5fcc87bd 100644 --- a/src/runtime/test_runner/expect/toBeInstanceOf.zig +++ b/src/runtime/test_runner/expect/toBeInstanceOf.zig @@ -19,7 +19,7 @@ pub fn toBeInstanceOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca } expected_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeInstanceOf", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeInstanceOf", "expected"); const not = this.flags.not; var pass = value.isInstanceOf(globalThis, expected_value); diff --git a/src/runtime/test_runner/expect/toBeInteger.zig b/src/runtime/test_runner/expect/toBeInteger.zig index 57dfdbd7196..320f6991a37 100644 --- a/src/runtime/test_runner/expect/toBeInteger.zig +++ b/src/runtime/test_runner/expect/toBeInteger.zig @@ -2,7 +2,7 @@ pub fn toBeInteger(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeInteger", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeInteger", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeLessThan.zig b/src/runtime/test_runner/expect/toBeLessThan.zig index f58e6994246..9ad24ba3605 100644 --- a/src/runtime/test_runner/expect/toBeLessThan.zig +++ b/src/runtime/test_runner/expect/toBeLessThan.zig @@ -14,7 +14,7 @@ pub fn toBeLessThan(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeLessThan", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeLessThan", "expected"); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig b/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig index 09198f45f4e..60cbe66f8c3 100644 --- a/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig +++ b/src/runtime/test_runner/expect/toBeLessThanOrEqual.zig @@ -14,7 +14,7 @@ pub fn toBeLessThanOrEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame const other_value = arguments[0]; other_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeLessThanOrEqual", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeLessThanOrEqual", "expected"); if ((!value.isNumber() and !value.isBigInt()) or (!other_value.isNumber() and !other_value.isBigInt())) { return globalThis.throw("Expected and actual values must be numbers or bigints", .{}); diff --git a/src/runtime/test_runner/expect/toBeNaN.zig b/src/runtime/test_runner/expect/toBeNaN.zig index ce7c2dfde4f..b8f49b7a39f 100644 --- a/src/runtime/test_runner/expect/toBeNaN.zig +++ b/src/runtime/test_runner/expect/toBeNaN.zig @@ -2,7 +2,7 @@ pub fn toBeNaN(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNaN", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNaN", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNegative.zig b/src/runtime/test_runner/expect/toBeNegative.zig index ff09705342f..ee6a0ca36d3 100644 --- a/src/runtime/test_runner/expect/toBeNegative.zig +++ b/src/runtime/test_runner/expect/toBeNegative.zig @@ -2,7 +2,7 @@ pub fn toBeNegative(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNegative", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNegative", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNil.zig b/src/runtime/test_runner/expect/toBeNil.zig index 05f19fcd614..2b853154326 100644 --- a/src/runtime/test_runner/expect/toBeNil.zig +++ b/src/runtime/test_runner/expect/toBeNil.zig @@ -2,7 +2,7 @@ pub fn toBeNil(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNil", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNil", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNull.zig b/src/runtime/test_runner/expect/toBeNull.zig index e2882b1ff86..1a1f05d35cb 100644 --- a/src/runtime/test_runner/expect/toBeNull.zig +++ b/src/runtime/test_runner/expect/toBeNull.zig @@ -2,7 +2,7 @@ pub fn toBeNull(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNull", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNull", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeNumber.zig b/src/runtime/test_runner/expect/toBeNumber.zig index 6283d35bb8f..cb94f40860a 100644 --- a/src/runtime/test_runner/expect/toBeNumber.zig +++ b/src/runtime/test_runner/expect/toBeNumber.zig @@ -2,7 +2,7 @@ pub fn toBeNumber(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeNumber", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeNumber", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeObject.rs b/src/runtime/test_runner/expect/toBeObject.rs index 66aaeba27f9..08fe3249a54 100644 --- a/src/runtime/test_runner/expect/toBeObject.rs +++ b/src/runtime/test_runner/expect/toBeObject.rs @@ -15,7 +15,7 @@ impl Expect { // (incl. `?` early-returns) is covered without a raw `*mut Expect`. let result = (|| -> JsResult { let this_value = frame.this(); - let value: JSValue = this.get_value(global, this_value, "toBeObject", "")?; + let value: JSValue = crate::ready_value!(this.get_value(global, this_value, frame, "toBeObject", "")?); this.increment_expect_call_counter(); diff --git a/src/runtime/test_runner/expect/toBeObject.zig b/src/runtime/test_runner/expect/toBeObject.zig index 12b0d94fbe5..474b946db90 100644 --- a/src/runtime/test_runner/expect/toBeObject.zig +++ b/src/runtime/test_runner/expect/toBeObject.zig @@ -2,7 +2,7 @@ pub fn toBeObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeObject", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeObject", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeOdd.zig b/src/runtime/test_runner/expect/toBeOdd.zig index f826d563838..a0cb5b0a75f 100644 --- a/src/runtime/test_runner/expect/toBeOdd.zig +++ b/src/runtime/test_runner/expect/toBeOdd.zig @@ -3,7 +3,7 @@ pub fn toBeOdd(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeOdd", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeOdd", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeOneOf.rs b/src/runtime/test_runner/expect/toBeOneOf.rs index a9689d2e455..b32d0383e63 100644 --- a/src/runtime/test_runner/expect/toBeOneOf.rs +++ b/src/runtime/test_runner/expect/toBeOneOf.rs @@ -36,8 +36,7 @@ pub(crate) fn to_be_one_of( global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { - let (this, expected, not) = - this.matcher_prelude(global_this, call_frame.this(), "toBeOneOf", "expected")?; + let (this, expected, not) = crate::ready_matcher!(this.matcher_prelude(global_this, call_frame.this(), call_frame, "toBeOneOf", "expected")?); let arguments_ = call_frame.arguments_old::<1>(); let arguments = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toBeOneOf.zig b/src/runtime/test_runner/expect/toBeOneOf.zig index 6eb9a500305..17b05f4bc84 100644 --- a/src/runtime/test_runner/expect/toBeOneOf.zig +++ b/src/runtime/test_runner/expect/toBeOneOf.zig @@ -14,7 +14,7 @@ pub fn toBeOneOf( this.incrementExpectCallCounter(); - const expected = (try this.getValue(globalThis, thisValue, callFrame, "toBeOneOf", "expected")) orelse return this.deferredResult(thisValue); + const expected = try this.getValue(globalThis, thisValue, "toBeOneOf", "expected"); const list_value: JSValue = arguments[0]; const not = this.flags.not; diff --git a/src/runtime/test_runner/expect/toBePositive.zig b/src/runtime/test_runner/expect/toBePositive.zig index 6384fc77bad..620090800d1 100644 --- a/src/runtime/test_runner/expect/toBePositive.zig +++ b/src/runtime/test_runner/expect/toBePositive.zig @@ -2,7 +2,7 @@ pub fn toBePositive(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Call defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBePositive", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBePositive", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeString.zig b/src/runtime/test_runner/expect/toBeString.zig index f75f1026a46..def65673efd 100644 --- a/src/runtime/test_runner/expect/toBeString.zig +++ b/src/runtime/test_runner/expect/toBeString.zig @@ -2,7 +2,7 @@ pub fn toBeString(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeString", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeString", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeSymbol.zig b/src/runtime/test_runner/expect/toBeSymbol.zig index fbd9b1f9c26..be06ed94090 100644 --- a/src/runtime/test_runner/expect/toBeSymbol.zig +++ b/src/runtime/test_runner/expect/toBeSymbol.zig @@ -2,7 +2,7 @@ pub fn toBeSymbol(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeSymbol", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeSymbol", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTrue.zig b/src/runtime/test_runner/expect/toBeTrue.zig index e101e524840..1764550df10 100644 --- a/src/runtime/test_runner/expect/toBeTrue.zig +++ b/src/runtime/test_runner/expect/toBeTrue.zig @@ -2,7 +2,7 @@ pub fn toBeTrue(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFram defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTrue", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTrue", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTruthy.zig b/src/runtime/test_runner/expect/toBeTruthy.zig index 7ac5363a7ec..e6043e60ef6 100644 --- a/src/runtime/test_runner/expect/toBeTruthy.zig +++ b/src/runtime/test_runner/expect/toBeTruthy.zig @@ -1,7 +1,7 @@ pub fn toBeTruthy(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTruthy", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTruthy", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeTypeOf.rs b/src/runtime/test_runner/expect/toBeTypeOf.rs index 6726efd7695..8270b9b8163 100644 --- a/src/runtime/test_runner/expect/toBeTypeOf.rs +++ b/src/runtime/test_runner/expect/toBeTypeOf.rs @@ -20,7 +20,7 @@ pub(crate) fn to_be_type_of( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = this.matcher_prelude(global, frame.this(), "toBeTypeOf", "")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toBeTypeOf", "")?); let _arguments = frame.arguments_old::<1>(); let arguments = _arguments.slice(); diff --git a/src/runtime/test_runner/expect/toBeTypeOf.zig b/src/runtime/test_runner/expect/toBeTypeOf.zig index 19ff4808754..ccc7773d4ed 100644 --- a/src/runtime/test_runner/expect/toBeTypeOf.zig +++ b/src/runtime/test_runner/expect/toBeTypeOf.zig @@ -20,7 +20,7 @@ pub fn toBeTypeOf(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr return globalThis.throwInvalidArguments("toBeTypeOf() requires 1 argument", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeTypeOf", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeTypeOf", ""); const expected = arguments[0]; expected.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toBeUndefined.zig b/src/runtime/test_runner/expect/toBeUndefined.zig index cf8f405d718..2230519f5a6 100644 --- a/src/runtime/test_runner/expect/toBeUndefined.zig +++ b/src/runtime/test_runner/expect/toBeUndefined.zig @@ -1,7 +1,7 @@ pub fn toBeUndefined(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue { defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeUndefined", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeUndefined", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeValidDate.rs b/src/runtime/test_runner/expect/toBeValidDate.rs index c9491e79594..a92074c00bc 100644 --- a/src/runtime/test_runner/expect/toBeValidDate.rs +++ b/src/runtime/test_runner/expect/toBeValidDate.rs @@ -12,7 +12,7 @@ pub(crate) fn to_be_valid_date( frame: &CallFrame, ) -> JsResult { let this_value = frame.this(); - let (this, value, not) = this.matcher_prelude(global, this_value, "toBeValidDate", "")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, this_value, frame, "toBeValidDate", "")?); let mut pass = value.is_date() && !value.get_unix_timestamp().is_nan(); if not { pass = !pass; diff --git a/src/runtime/test_runner/expect/toBeValidDate.zig b/src/runtime/test_runner/expect/toBeValidDate.zig index d323c445dad..b274faec228 100644 --- a/src/runtime/test_runner/expect/toBeValidDate.zig +++ b/src/runtime/test_runner/expect/toBeValidDate.zig @@ -2,7 +2,7 @@ pub fn toBeValidDate(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal defer this.postMatch(globalThis); const thisValue = callFrame.this(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeValidDate", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeValidDate", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toBeWithin.rs b/src/runtime/test_runner/expect/toBeWithin.rs index 3de2c0c4170..5f9945d9543 100644 --- a/src/runtime/test_runner/expect/toBeWithin.rs +++ b/src/runtime/test_runner/expect/toBeWithin.rs @@ -9,12 +9,13 @@ impl Expect { global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = self.matcher_prelude( + let (this, value, not) = crate::ready_matcher!(self.matcher_prelude( global, frame.this(), + frame, "toBeWithin", "start, end", - )?; + )?); let _arguments = frame.arguments_old::<2>(); let arguments = _arguments.slice(); diff --git a/src/runtime/test_runner/expect/toBeWithin.zig b/src/runtime/test_runner/expect/toBeWithin.zig index 1e6dc6bd622..772aef3cd55 100644 --- a/src/runtime/test_runner/expect/toBeWithin.zig +++ b/src/runtime/test_runner/expect/toBeWithin.zig @@ -9,7 +9,7 @@ pub fn toBeWithin(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFr return globalThis.throwInvalidArguments("toBeWithin() requires 2 arguments", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toBeWithin", "start, end")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toBeWithin", "start, end"); const startValue = arguments[0]; startValue.ensureStillAlive(); diff --git a/src/runtime/test_runner/expect/toContain.rs b/src/runtime/test_runner/expect/toContain.rs index ea9926fe4c8..2805c178470 100644 --- a/src/runtime/test_runner/expect/toContain.rs +++ b/src/runtime/test_runner/expect/toContain.rs @@ -13,8 +13,7 @@ impl Expect { global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toContain", "expected")?; + let (this, value, not) = crate::ready_matcher!(self.matcher_prelude(global, frame.this(), frame, "toContain", "expected")?); let arguments_ = frame.arguments_old::<1>(); let arguments = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toContain.zig b/src/runtime/test_runner/expect/toContain.zig index dd32530b8e1..45b6d44c8f2 100644 --- a/src/runtime/test_runner/expect/toContain.zig +++ b/src/runtime/test_runner/expect/toContain.zig @@ -16,7 +16,7 @@ pub fn toContain( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContain", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toContain", "expected"); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainAllKeys.zig b/src/runtime/test_runner/expect/toContainAllKeys.zig index ea656d4f1b8..f494e5ea436 100644 --- a/src/runtime/test_runner/expect/toContainAllKeys.zig +++ b/src/runtime/test_runner/expect/toContainAllKeys.zig @@ -16,7 +16,7 @@ pub fn toContainAllKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAllKeys", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAllKeys", "expected"); if (!expected.jsType().isArray()) { return globalObject.throwInvalidArgumentType("toContainAllKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainAllValues.zig b/src/runtime/test_runner/expect/toContainAllValues.zig index dd0fc1c9ea4..3f8cb53993a 100644 --- a/src/runtime/test_runner/expect/toContainAllValues.zig +++ b/src/runtime/test_runner/expect/toContainAllValues.zig @@ -19,7 +19,7 @@ pub fn toContainAllValues( return globalObject.throwInvalidArgumentType("toContainAllValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAllValues", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAllValues", "expected"); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainAnyKeys.zig b/src/runtime/test_runner/expect/toContainAnyKeys.zig index b5ecbb0ffba..aaebf75f9ae 100644 --- a/src/runtime/test_runner/expect/toContainAnyKeys.zig +++ b/src/runtime/test_runner/expect/toContainAnyKeys.zig @@ -16,7 +16,7 @@ pub fn toContainAnyKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainAnyKeys", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toContainAnyKeys", "expected"); if (!expected.jsType().isArray()) { return globalThis.throwInvalidArgumentType("toContainAnyKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainAnyValues.zig b/src/runtime/test_runner/expect/toContainAnyValues.zig index 022cc21c2ab..ea3dc18c6a7 100644 --- a/src/runtime/test_runner/expect/toContainAnyValues.zig +++ b/src/runtime/test_runner/expect/toContainAnyValues.zig @@ -19,7 +19,7 @@ pub fn toContainAnyValues( return globalObject.throwInvalidArgumentType("toContainAnyValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainAnyValues", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalObject, thisValue, "toContainAnyValues", "expected"); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainEqual.rs b/src/runtime/test_runner/expect/toContainEqual.rs index c4384459e52..595426b3e87 100644 --- a/src/runtime/test_runner/expect/toContainEqual.rs +++ b/src/runtime/test_runner/expect/toContainEqual.rs @@ -36,8 +36,7 @@ pub(crate) fn to_contain_equal( frame: &CallFrame, ) -> JsResult { let this_value = frame.this(); - let (this, value, not) = - this.matcher_prelude(global, this_value, "toContainEqual", "expected")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, this_value, frame, "toContainEqual", "expected")?); let arguments_ = frame.arguments_old::<1>(); let arguments = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toContainEqual.zig b/src/runtime/test_runner/expect/toContainEqual.zig index 834b7f8045f..b4b4e69cda6 100644 --- a/src/runtime/test_runner/expect/toContainEqual.zig +++ b/src/runtime/test_runner/expect/toContainEqual.zig @@ -16,7 +16,7 @@ pub fn toContainEqual( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainEqual", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toContainEqual", "expected"); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainKey.zig b/src/runtime/test_runner/expect/toContainKey.zig index 9dcebf66b9f..0121e85c8a3 100644 --- a/src/runtime/test_runner/expect/toContainKey.zig +++ b/src/runtime/test_runner/expect/toContainKey.zig @@ -16,7 +16,7 @@ pub fn toContainKey( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainKey", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toContainKey", "expected"); var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; defer formatter.deinit(); diff --git a/src/runtime/test_runner/expect/toContainKeys.zig b/src/runtime/test_runner/expect/toContainKeys.zig index d3099456d77..eef143f81cb 100644 --- a/src/runtime/test_runner/expect/toContainKeys.zig +++ b/src/runtime/test_runner/expect/toContainKeys.zig @@ -16,7 +16,7 @@ pub fn toContainKeys( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toContainKeys", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toContainKeys", "expected"); if (!expected.jsType().isArray()) { return globalThis.throwInvalidArgumentType("toContainKeys", "expected", "array"); diff --git a/src/runtime/test_runner/expect/toContainValue.zig b/src/runtime/test_runner/expect/toContainValue.zig index dfdc7550cfa..31f92c96bed 100644 --- a/src/runtime/test_runner/expect/toContainValue.zig +++ b/src/runtime/test_runner/expect/toContainValue.zig @@ -16,7 +16,7 @@ pub fn toContainValue( const expected = arguments[0]; expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainValue", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalObject, thisValue, "toContainValue", "expected"); const not = this.flags.not; var pass = false; diff --git a/src/runtime/test_runner/expect/toContainValues.zig b/src/runtime/test_runner/expect/toContainValues.zig index 751b5fa24b9..567bd7e1b80 100644 --- a/src/runtime/test_runner/expect/toContainValues.zig +++ b/src/runtime/test_runner/expect/toContainValues.zig @@ -19,7 +19,7 @@ pub fn toContainValues( return globalObject.throwInvalidArgumentType("toContainValues", "expected", "array"); } expected.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalObject, thisValue, callFrame, "toContainValues", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalObject, thisValue, "toContainValues", "expected"); const not = this.flags.not; var pass = true; diff --git a/src/runtime/test_runner/expect/toEndWith.zig b/src/runtime/test_runner/expect/toEndWith.zig index d3b65536b4a..0bdea7e46da 100644 --- a/src/runtime/test_runner/expect/toEndWith.zig +++ b/src/runtime/test_runner/expect/toEndWith.zig @@ -16,7 +16,7 @@ pub fn toEndWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra return globalThis.throw("toEndWith() requires the first argument to be a string", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEndWith", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toEndWith", "expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toEqual.rs b/src/runtime/test_runner/expect/toEqual.rs index 8cc083a3f7c..26ce0f9b082 100644 --- a/src/runtime/test_runner/expect/toEqual.rs +++ b/src/runtime/test_runner/expect/toEqual.rs @@ -10,8 +10,7 @@ impl Expect { global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toEqual", "expected")?; + let (this, value, not) = crate::ready_matcher!(self.matcher_prelude(global, frame.this(), frame, "toEqual", "expected")?); let _arguments = frame.arguments_old::<1>(); let arguments: &[JSValue] = _arguments.slice(); diff --git a/src/runtime/test_runner/expect/toEqual.zig b/src/runtime/test_runner/expect/toEqual.zig index 6ce89298984..614b9fc549a 100644 --- a/src/runtime/test_runner/expect/toEqual.zig +++ b/src/runtime/test_runner/expect/toEqual.zig @@ -12,7 +12,7 @@ pub fn toEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEqual", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toEqual", "expected"); const not = this.flags.not; var pass = try value.jestDeepEquals(expected, globalThis); diff --git a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.rs b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.rs index c8d3ecafb46..2416eda95e4 100644 --- a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.rs +++ b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.rs @@ -15,8 +15,7 @@ pub(crate) fn to_equal_ignoring_whitespace( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - this.matcher_prelude(global, frame.this(), "toEqualIgnoringWhitespace", "expected")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toEqualIgnoringWhitespace", "expected")?); let arguments_ = frame.arguments_old::<1>(); let arguments: &[JSValue] = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig index e14cb18ba4f..18acdaeb078 100644 --- a/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig +++ b/src/runtime/test_runner/expect/toEqualIgnoringWhitespace.zig @@ -12,7 +12,7 @@ pub fn toEqualIgnoringWhitespace(this: *Expect, globalThis: *JSGlobalObject, cal this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toEqualIgnoringWhitespace", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toEqualIgnoringWhitespace", "expected"); if (!expected.isString()) { return globalThis.throw("toEqualIgnoringWhitespace() requires argument to be a string", .{}); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalled.rs b/src/runtime/test_runner/expect/toHaveBeenCalled.rs index ddebfb26eab..f1399a77adc 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalled.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalled.rs @@ -7,8 +7,7 @@ pub(crate) fn to_have_been_called( frame: &CallFrame, ) -> JsResult { bun_jsc::mark_binding!(); - let (this, calls, _value) = - this.mock_prologue(global, frame.this(), "toHaveBeenCalled", "", super::mock::MockKind::Calls)?; + let (this, calls, _value) = crate::ready_mock!(this.mock_prologue(global, frame, "toHaveBeenCalled", "", super::mock::MockKind::Calls)?); // arg-check after prologue: counter bump + post_match still fire on bad-arity. if !frame.arguments_as_array::<1>()[0].is_undefined() { return Err(global.throw_invalid_arguments(format_args!( diff --git a/src/runtime/test_runner/expect/toHaveBeenCalled.zig b/src/runtime/test_runner/expect/toHaveBeenCalled.zig index f13160b1c5c..a4113d1f045 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalled.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalled.zig @@ -8,7 +8,7 @@ pub fn toHaveBeenCalled(this: *Expect, globalThis: *JSGlobalObject, callframe: * return globalThis.throwInvalidArguments("toHaveBeenCalled() must not have an argument", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalled", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalled", ""); const calls = try bun.cpp.JSMockFunction__getCalls(globalThis, value); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.rs b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.rs index 1d1f99e6b15..be8b99dac99 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.rs @@ -8,13 +8,9 @@ pub(crate) fn to_have_been_called_once( frame: &CallFrame, ) -> JsResult { bun_jsc::mark_binding!(); - let (this, calls, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveBeenCalledOnce", - "expected", - super::mock::MockKind::Calls, - )?; + let (this, calls, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveBeenCalledOnce", "expected", super::mock::MockKind::Calls, + )?); let calls_length = calls.get_length(global)?; let mut pass = calls_length == 1; diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig index 18d206e7d6e..dbb20dac0ce 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledOnce.zig @@ -3,7 +3,7 @@ pub fn toHaveBeenCalledOnce(this: *Expect, globalThis: *JSGlobalObject, callfram const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledOnce", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledOnce", "expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.rs b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.rs index c903e69ae19..802e0797055 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.rs @@ -9,13 +9,9 @@ pub(crate) fn to_have_been_called_times( ) -> JsResult { let arguments_ = frame.arguments_old::<1>(); let arguments: &[JSValue] = arguments_.slice(); - let (this, calls, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveBeenCalledTimes", - "expected", - super::mock::MockKind::Calls, - )?; + let (this, calls, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveBeenCalledTimes", "expected", super::mock::MockKind::Calls, + )?); if arguments.len() < 1 || !arguments[0].is_uint32_as_any_int() { return Err(global.throw_invalid_arguments(format_args!( diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig index 62427dae5a2..5b0db03abaa 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledTimes.zig @@ -5,7 +5,7 @@ pub fn toHaveBeenCalledTimes(this: *Expect, globalThis: *JSGlobalObject, callfra const arguments_ = callframe.arguments_old(1); const arguments: []const JSValue = arguments_.slice(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledTimes", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledTimes", "expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs index defb78cb26f..140aacf14dc 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenCalledWith.rs @@ -11,13 +11,9 @@ pub(crate) fn to_have_been_called_with( ) -> JsResult { bun_jsc::mark_binding!(); let arguments = frame.arguments(); - let (this, calls, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveBeenCalledWith", - "...expected", - mock::MockKind::CallsWithSig, - )?; + let (this, calls, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveBeenCalledWith", "...expected", mock::MockKind::CallsWithSig, + )?); let mut pass = false; diff --git a/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig index f17d6f15070..d49c572394b 100644 --- a/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenCalledWith(this: *Expect, globalThis: *JSGlobalObject, callfram const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenCalledWith", "...expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenCalledWith", "...expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs index 735c24befba..32385102f7e 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs @@ -10,13 +10,9 @@ pub(crate) fn to_have_been_last_called_with( ) -> JsResult { bun_jsc::mark_binding!(); let arguments = frame.arguments(); - let (this, calls, value) = this.mock_prologue( - global, - frame.this(), - "toHaveBeenLastCalledWith", - "...expected", - super::mock::MockKind::CallsWithSig, - )?; + let (this, calls, value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveBeenLastCalledWith", "...expected", super::mock::MockKind::CallsWithSig, + )?); let total_calls: u32 = calls.get_length(global)? as u32; let mut last_call_value: JSValue = JSValue::ZERO; diff --git a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig index 4da2e02a5a4..488a42916e0 100644 --- a/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenLastCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenLastCalledWith(this: *Expect, globalThis: *JSGlobalObject, call const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenLastCalledWith", "...expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastCalledWith", "...expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs index fe270de99f3..6e799cf2d65 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs @@ -8,13 +8,9 @@ pub(crate) fn to_have_been_nth_called_with( frame: &CallFrame, ) -> JsResult { let arguments = frame.arguments(); - let (this, calls, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveBeenNthCalledWith", - "n, ...expected", - super::mock::MockKind::CallsWithSig, - )?; + let (this, calls, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveBeenNthCalledWith", "n, ...expected", super::mock::MockKind::CallsWithSig, + )?); if arguments.is_empty() || !arguments[0].is_any_int() { return Err(global.throw_invalid_arguments(format_args!( diff --git a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig index ccf30eaaf13..9ebcd9cb280 100644 --- a/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig +++ b/src/runtime/test_runner/expect/toHaveBeenNthCalledWith.zig @@ -4,7 +4,7 @@ pub fn toHaveBeenNthCalledWith(this: *Expect, globalThis: *JSGlobalObject, callf const thisValue = callframe.this(); const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenNthCalledWith", "n, ...expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenNthCalledWith", "n, ...expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs index 750bd4b81ee..4b4c079d11c 100644 --- a/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveLastReturnedWith.rs @@ -12,13 +12,9 @@ pub(crate) fn to_have_last_returned_with( ) -> JsResult { bun_jsc::mark_binding!(); let expected = callframe.arguments_as_array::<1>()[0]; - let (this, returns, _value) = this.mock_prologue( - global_this, - callframe.this(), - "toHaveBeenLastReturnedWith", - "expected", - super::mock::MockKind::Returns, - )?; + let (this, returns, _value) = crate::ready_mock!(this.mock_prologue( + global_this, callframe, "toHaveBeenLastReturnedWith", "expected", super::mock::MockKind::Returns, + )?); let calls_count = u32::try_from(returns.get_length(global_this)?).unwrap(); let mut pass = false; diff --git a/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig b/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig index 6608f5c4d75..82b7230a61a 100644 --- a/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveLastReturnedWith.zig @@ -4,7 +4,7 @@ pub fn toHaveLastReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfr const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveBeenLastReturnedWith", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveBeenLastReturnedWith", "expected"); const expected = callframe.argumentsAsArray(1)[0]; this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveLength.rs b/src/runtime/test_runner/expect/toHaveLength.rs index feedb825882..be6f4cdea4f 100644 --- a/src/runtime/test_runner/expect/toHaveLength.rs +++ b/src/runtime/test_runner/expect/toHaveLength.rs @@ -8,8 +8,7 @@ pub(crate) fn to_have_length( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - this.matcher_prelude(global, frame.this(), "toHaveLength", "expected")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toHaveLength", "expected")?); let arguments_ = frame.arguments_old::<1>(); let arguments = arguments_.slice(); diff --git a/src/runtime/test_runner/expect/toHaveLength.zig b/src/runtime/test_runner/expect/toHaveLength.zig index fcb1b107b4a..5aa7a349ef9 100644 --- a/src/runtime/test_runner/expect/toHaveLength.zig +++ b/src/runtime/test_runner/expect/toHaveLength.zig @@ -15,7 +15,7 @@ pub fn toHaveLength( this.incrementExpectCallCounter(); const expected: JSValue = arguments[0]; - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveLength", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveLength", "expected"); if (!value.isObject() and !value.isString()) { var fmt = jsc.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true }; diff --git a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs index 8367a6e9c85..1995a7e798f 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.rs @@ -10,13 +10,9 @@ pub(crate) fn to_have_nth_returned_with( ) -> JsResult { bun_jsc::mark_binding!(); let [nth_arg, expected] = frame.arguments_as_array::<2>(); - let (this, returns, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveNthReturnedWith", - "n, expected", - super::mock::MockKind::Returns, - )?; + let (this, returns, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveNthReturnedWith", "n, expected", super::mock::MockKind::Returns, + )?); // Validate n is a number if !nth_arg.is_any_int() { diff --git a/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig b/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig index 5c0cfae6ea2..5ba76d46a07 100644 --- a/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveNthReturnedWith.zig @@ -2,7 +2,7 @@ pub fn toHaveNthReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callfra jsc.markBinding(@src()); const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveNthReturnedWith", "n, expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveNthReturnedWith", "n, expected"); const nth_arg, const expected = callframe.argumentsAsArray(2); diff --git a/src/runtime/test_runner/expect/toHaveProperty.rs b/src/runtime/test_runner/expect/toHaveProperty.rs index c7888375793..443c7120a41 100644 --- a/src/runtime/test_runner/expect/toHaveProperty.rs +++ b/src/runtime/test_runner/expect/toHaveProperty.rs @@ -31,12 +31,13 @@ pub(crate) fn to_have_property( ev.ensure_still_alive(); } - let value: JSValue = this.get_value( + let value: JSValue = crate::ready_value!(this.get_value( global, this_value, + frame, "toHaveProperty", "path, value", - )?; + )?); if !expected_property_path.is_string() && !expected_property_path.is_iterable(global)? { return Err(global.throw(format_args!("Expected path must be a string or an array"))); diff --git a/src/runtime/test_runner/expect/toHaveProperty.zig b/src/runtime/test_runner/expect/toHaveProperty.zig index ce326f35c4d..019c815511e 100644 --- a/src/runtime/test_runner/expect/toHaveProperty.zig +++ b/src/runtime/test_runner/expect/toHaveProperty.zig @@ -16,7 +16,7 @@ pub fn toHaveProperty(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Ca const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null; if (expected_property) |ev| ev.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toHaveProperty", "path, value")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveProperty", "path, value"); if (!expected_property_path.isString() and !try expected_property_path.isIterable(globalThis)) { return globalThis.throw("Expected path must be a string or an array", .{}); diff --git a/src/runtime/test_runner/expect/toHaveReturned.rs b/src/runtime/test_runner/expect/toHaveReturned.rs index a5ea4977318..a7c98f7b368 100644 --- a/src/runtime/test_runner/expect/toHaveReturned.rs +++ b/src/runtime/test_runner/expect/toHaveReturned.rs @@ -29,13 +29,13 @@ fn to_have_returned_times_fn( ) -> JsResult { bun_jsc::mark_binding!(); let arguments = callframe.arguments(); - let (this, returns_arr, _value) = this.mock_prologue( + let (this, returns_arr, _value) = crate::ready_mock!(this.mock_prologue( global, - callframe.this(), + callframe, mode.tag_name(), "expected", mock::MockKind::Returns, - )?; + )?); let mut returns = returns_arr.array_iterator(global)?; let expected_success_count: i32 = if mode == Mode::ToHaveReturned { diff --git a/src/runtime/test_runner/expect/toHaveReturned.zig b/src/runtime/test_runner/expect/toHaveReturned.zig index 3c842dcb971..75438185fcd 100644 --- a/src/runtime/test_runner/expect/toHaveReturned.zig +++ b/src/runtime/test_runner/expect/toHaveReturned.zig @@ -5,7 +5,7 @@ inline fn toHaveReturnedTimesFn(this: *Expect, globalThis: *JSGlobalObject, call const arguments = callframe.arguments(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, @tagName(mode), "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, @tagName(mode), "expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toHaveReturnedWith.rs b/src/runtime/test_runner/expect/toHaveReturnedWith.rs index d6bcd036583..d94b605f8b6 100644 --- a/src/runtime/test_runner/expect/toHaveReturnedWith.rs +++ b/src/runtime/test_runner/expect/toHaveReturnedWith.rs @@ -10,13 +10,9 @@ pub(crate) fn to_have_returned_with( frame: &CallFrame, ) -> JsResult { let expected = frame.arguments_as_array::<1>()[0]; - let (this, returns, _value) = this.mock_prologue( - global, - frame.this(), - "toHaveReturnedWith", - "expected", - mock::MockKind::Returns, - )?; + let (this, returns, _value) = crate::ready_mock!(this.mock_prologue( + global, frame, "toHaveReturnedWith", "expected", mock::MockKind::Returns, + )?); let calls_count = u32::try_from(returns.get_length(global)?).unwrap(); let mut pass = false; diff --git a/src/runtime/test_runner/expect/toHaveReturnedWith.zig b/src/runtime/test_runner/expect/toHaveReturnedWith.zig index 0a20538506a..c8aac164a5b 100644 --- a/src/runtime/test_runner/expect/toHaveReturnedWith.zig +++ b/src/runtime/test_runner/expect/toHaveReturnedWith.zig @@ -4,7 +4,7 @@ pub fn toHaveReturnedWith(this: *Expect, globalThis: *JSGlobalObject, callframe: const thisValue = callframe.this(); defer this.postMatch(globalThis); - const value: JSValue = (try this.getValue(globalThis, thisValue, callframe, "toHaveReturnedWith", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toHaveReturnedWith", "expected"); const expected = callframe.argumentsAsArray(1)[0]; this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toInclude.zig b/src/runtime/test_runner/expect/toInclude.zig index 204a9834abf..fe8711f0d60 100644 --- a/src/runtime/test_runner/expect/toInclude.zig +++ b/src/runtime/test_runner/expect/toInclude.zig @@ -16,7 +16,7 @@ pub fn toInclude(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFra return globalThis.throw("toInclude() requires the first argument to be a string", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toInclude", "")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toInclude", ""); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toMatch.rs b/src/runtime/test_runner/expect/toMatch.rs index c7beace723e..8df7aed0e7a 100644 --- a/src/runtime/test_runner/expect/toMatch.rs +++ b/src/runtime/test_runner/expect/toMatch.rs @@ -8,8 +8,7 @@ pub(crate) fn to_match( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - this.matcher_prelude(global, frame.this(), "toMatch", "expected")?; + let (this, value, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toMatch", "expected")?); let arguments: &[JSValue] = frame.arguments(); diff --git a/src/runtime/test_runner/expect/toMatch.zig b/src/runtime/test_runner/expect/toMatch.zig index 67501f41af8..3b8d877dccc 100644 --- a/src/runtime/test_runner/expect/toMatch.zig +++ b/src/runtime/test_runner/expect/toMatch.zig @@ -22,7 +22,7 @@ pub fn toMatch(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame } expected_value.ensureStillAlive(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatch", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toMatch", "expected"); if (!value.isString()) { return globalThis.throw("Received value must be a string: {f}", .{value.toFmt(&formatter)}); diff --git a/src/runtime/test_runner/expect/toMatchInlineSnapshot.rs b/src/runtime/test_runner/expect/toMatchInlineSnapshot.rs index 1bf737fd5e6..4f6dc3f23c7 100644 --- a/src/runtime/test_runner/expect/toMatchInlineSnapshot.rs +++ b/src/runtime/test_runner/expect/toMatchInlineSnapshot.rs @@ -81,12 +81,13 @@ pub(crate) fn to_match_inline_snapshot( let expected_slice: Option<&[u8]> = if has_expected { Some(expected.slice()) } else { None }; - let value = this.get_value( + let value = crate::ready_value!(this.get_value( global, this_value, + frame, "toMatchInlineSnapshot", "properties, hint", - )?; + )?); Expect::inline_snapshot( &**this, global, diff --git a/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig b/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig index 44e9c73efdb..92faf91ffec 100644 --- a/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig +++ b/src/runtime/test_runner/expect/toMatchInlineSnapshot.zig @@ -47,7 +47,7 @@ pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFra const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; - const value = (try this.getValue(globalThis, thisValue, callFrame, "toMatchInlineSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); + const value = try this.getValue(globalThis, thisValue, "toMatchInlineSnapshot", "properties, hint"); return this.inlineSnapshot(globalThis, callFrame, value, property_matchers, expected_slice, "toMatchInlineSnapshot"); } diff --git a/src/runtime/test_runner/expect/toMatchObject.rs b/src/runtime/test_runner/expect/toMatchObject.rs index 411363d53c2..290f27fd236 100644 --- a/src/runtime/test_runner/expect/toMatchObject.rs +++ b/src/runtime/test_runner/expect/toMatchObject.rs @@ -7,8 +7,7 @@ pub(crate) fn to_match_object( global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, received_object, not) = - this.matcher_prelude(global, frame.this(), "toMatchObject", "expected")?; + let (this, received_object, not) = crate::ready_matcher!(this.matcher_prelude(global, frame.this(), frame, "toMatchObject", "expected")?); let args_buf = frame.arguments_old::<1>(); let args = args_buf.slice(); diff --git a/src/runtime/test_runner/expect/toMatchObject.zig b/src/runtime/test_runner/expect/toMatchObject.zig index e8e3557141e..e9b065e05ae 100644 --- a/src/runtime/test_runner/expect/toMatchObject.zig +++ b/src/runtime/test_runner/expect/toMatchObject.zig @@ -9,7 +9,7 @@ pub fn toMatchObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal const not = this.flags.not; - const received_object: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatchObject", "expected")) orelse return this.deferredResult(thisValue); + const received_object: JSValue = try this.getValue(globalThis, thisValue, "toMatchObject", "expected"); if (!received_object.isObject()) { const matcher_error = "\n\nMatcher error: received value must be a non-null object\n"; diff --git a/src/runtime/test_runner/expect/toMatchSnapshot.rs b/src/runtime/test_runner/expect/toMatchSnapshot.rs index cae5037fada..d74ab708c1b 100644 --- a/src/runtime/test_runner/expect/toMatchSnapshot.rs +++ b/src/runtime/test_runner/expect/toMatchSnapshot.rs @@ -90,12 +90,13 @@ pub(crate) fn to_match_snapshot( let hint = hint_string.to_slice(); // `hint` cleanup handled by Drop. - let value: JSValue = this.get_value( + let value: JSValue = crate::ready_value!(this.get_value( global, this_value, + frame, "toMatchSnapshot", "properties, hint", - )?; + )?); Expect::snapshot(&**this, global, value, property_matchers, hint.slice(), "toMatchSnapshot") } diff --git a/src/runtime/test_runner/expect/toMatchSnapshot.zig b/src/runtime/test_runner/expect/toMatchSnapshot.zig index f69a989a509..30de93411dc 100644 --- a/src/runtime/test_runner/expect/toMatchSnapshot.zig +++ b/src/runtime/test_runner/expect/toMatchSnapshot.zig @@ -50,7 +50,7 @@ pub fn toMatchSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *C var hint = hint_string.toSlice(default_allocator); defer hint.deinit(); - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toMatchSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toMatchSnapshot", "properties, hint"); return this.snapshot(globalThis, value, property_matchers, hint.slice(), "toMatchSnapshot"); } diff --git a/src/runtime/test_runner/expect/toStartWith.zig b/src/runtime/test_runner/expect/toStartWith.zig index 618d605c518..ddf71443417 100644 --- a/src/runtime/test_runner/expect/toStartWith.zig +++ b/src/runtime/test_runner/expect/toStartWith.zig @@ -16,7 +16,7 @@ pub fn toStartWith(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallF return globalThis.throw("toStartWith() requires the first argument to be a string", .{}); } - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toStartWith", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toStartWith", "expected"); this.incrementExpectCallCounter(); diff --git a/src/runtime/test_runner/expect/toStrictEqual.rs b/src/runtime/test_runner/expect/toStrictEqual.rs index 9c246be1022..d373e750e04 100644 --- a/src/runtime/test_runner/expect/toStrictEqual.rs +++ b/src/runtime/test_runner/expect/toStrictEqual.rs @@ -10,8 +10,7 @@ impl Expect { global: &JSGlobalObject, frame: &CallFrame, ) -> JsResult { - let (this, value, not) = - self.matcher_prelude(global, frame.this(), "toStrictEqual", "expected")?; + let (this, value, not) = crate::ready_matcher!(self.matcher_prelude(global, frame.this(), frame, "toStrictEqual", "expected")?); let _arguments = frame.arguments_old::<1>(); let arguments: &[JSValue] = _arguments.slice(); diff --git a/src/runtime/test_runner/expect/toStrictEqual.zig b/src/runtime/test_runner/expect/toStrictEqual.zig index 1b5ca42651c..47414be4775 100644 --- a/src/runtime/test_runner/expect/toStrictEqual.zig +++ b/src/runtime/test_runner/expect/toStrictEqual.zig @@ -12,7 +12,7 @@ pub fn toStrictEqual(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal this.incrementExpectCallCounter(); const expected = arguments[0]; - const value: JSValue = (try this.getValue(globalThis, thisValue, callFrame, "toStrictEqual", "expected")) orelse return this.deferredResult(thisValue); + const value: JSValue = try this.getValue(globalThis, thisValue, "toStrictEqual", "expected"); const not = this.flags.not; var pass = try value.jestStrictDeepEquals(expected, globalThis); diff --git a/src/runtime/test_runner/expect/toThrow.rs b/src/runtime/test_runner/expect/toThrow.rs index db44625079a..ed199ed8d8c 100644 --- a/src/runtime/test_runner/expect/toThrow.rs +++ b/src/runtime/test_runner/expect/toThrow.rs @@ -60,7 +60,7 @@ pub(crate) fn to_throw( let (result_, return_value_from_function) = this.get_value_as_to_throw( global, - this.get_value(global, this_value, "toThrow", "expected")?, + crate::ready_value!(this.get_value(global, this_value, frame, "toThrow", "expected")?), )?; let did_throw = result_.is_some(); diff --git a/src/runtime/test_runner/expect/toThrow.zig b/src/runtime/test_runner/expect/toThrow.zig index a8847977bcf..9927bea0bda 100644 --- a/src/runtime/test_runner/expect/toThrow.zig +++ b/src/runtime/test_runner/expect/toThrow.zig @@ -32,8 +32,7 @@ pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame const not = this.flags.not; - const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrow", "expected")) orelse return this.deferredResult(thisValue); - const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, received); + const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, try this.getValue(globalThis, thisValue, "toThrow", "expected")); const did_throw = result_ != null; diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.rs b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.rs index 4849a213f37..f2788219969 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.rs +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.rs @@ -59,12 +59,13 @@ pub(crate) fn to_throw_error_matching_inline_snapshot( // reshaped for borrowck — hoist get_value out so the two &mut self // receivers don't overlap. - let received = this.get_value( + let received = crate::ready_value!(this.get_value( global, this_value, + frame, "toThrowErrorMatchingInlineSnapshot", "properties, hint", - )?; + )?); let Some(value) = this.fn_to_err_string_or_undefined(global, received)? else { let signature = Expect::get_signature("toThrowErrorMatchingInlineSnapshot", "", false); return this.throw( diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig index c93d2d5b801..723a936c61d 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingInlineSnapshot.zig @@ -32,8 +32,7 @@ pub fn toThrowErrorMatchingInlineSnapshot(this: *Expect, globalThis: *JSGlobalOb const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null; - const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrowErrorMatchingInlineSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); - const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, received)) orelse { + const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingInlineSnapshot", "properties, hint"))) orelse { const signature = comptime getSignature("toThrowErrorMatchingInlineSnapshot", "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Received function did not throw\n", .{}); }; diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.rs b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.rs index a89d4c59025..a30611a8195 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.rs +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.rs @@ -70,12 +70,13 @@ pub(crate) fn to_throw_error_matching_snapshot( let Some(value): Option = this.fn_to_err_string_or_undefined( global, - this.get_value( - global, - this_value, - "toThrowErrorMatchingSnapshot", - "properties, hint", - )?, + crate::ready_value!(this.get_value( + global, + this_value, + frame, + "toThrowErrorMatchingSnapshot", + "properties, hint", + )?), )? else { let signature = get_signature("toThrowErrorMatchingSnapshot", "", false); diff --git a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig index 334dad51d7c..5162a14557e 100644 --- a/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig +++ b/src/runtime/test_runner/expect/toThrowErrorMatchingSnapshot.zig @@ -34,8 +34,7 @@ pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, var hint = hint_string.toSlice(default_allocator); defer hint.deinit(); - const received = (try this.getValue(globalThis, thisValue, callFrame, "toThrowErrorMatchingSnapshot", "properties, hint")) orelse return this.deferredResult(thisValue); - const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, received)) orelse { + const value: JSValue = (try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingSnapshot", "properties, hint"))) orelse { const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", false); return this.throw(globalThis, signature, "\n\nMatcher error: Received function did not throw\n", .{}); }; diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 5b16e24b857..efa124293fb 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -48,6 +48,45 @@ macro_rules! unary_predicate_matcher { }; } +/// Unwrap a [`expect_core::MatcherStart`] or early-return the deferred +/// promise. Used at the top of every matcher body that goes through +/// `matcher_prelude` so a `.resolves`/`.rejects` matcher on a still-pending +/// promise returns the deferred promise to its caller instead of blocking +/// the event loop (#14950). +#[macro_export] +macro_rules! ready_matcher { + ($start:expr) => { + match $start { + $crate::test_runner::expect_core::MatcherStart::Ready(t, v, n) => (t, v, n), + $crate::test_runner::expect_core::MatcherStart::Deferred(p) => return Ok(p), + } + }; +} + +/// Unwrap a [`expect_core::MaybeDeferred`] or early-return the deferred +/// promise. See [`ready_matcher!`]. +#[macro_export] +macro_rules! ready_value { + ($v:expr) => { + match $v { + $crate::test_runner::expect_core::MaybeDeferred::Value(v) => v, + $crate::test_runner::expect_core::MaybeDeferred::Deferred(p) => return Ok(p), + } + }; +} + +/// Unwrap a [`expect_core::mock::MockStart`] or early-return the deferred +/// promise. See [`ready_matcher!`]. +#[macro_export] +macro_rules! ready_mock { + ($start:expr) => { + match $start { + $crate::test_runner::expect_core::mock::MockStart::Ready(t, a, v) => (t, a, v), + $crate::test_runner::expect_core::mock::MockStart::Deferred(p) => return Ok(p), + } + }; +} + cfg_jsc! { #[path = "bun_test.rs"] pub mod bun_test; #[path = "Collection.rs"] pub mod collection; @@ -433,7 +472,7 @@ pub mod expect { other_value.ensure_still_alive(); let value: JSValue = - this.get_value(global, this_value, name, "expected")?; + crate::ready_value!(this.get_value(global, this_value, frame, name, "expected")?); if (!value.is_number() && !value.is_big_int()) || (!other_value.is_number() && !other_value.is_big_int()) diff --git a/test/js/bun/test/expect-resolves-pending.test.ts b/test/js/bun/test/expect-resolves-pending.test.ts index 4ba1286ccda..293b6b4e9a6 100644 --- a/test/js/bun/test/expect-resolves-pending.test.ts +++ b/test/js/bun/test/expect-resolves-pending.test.ts @@ -293,8 +293,10 @@ describe.concurrent("expect().resolves / .rejects on a still-pending promise", ( expect(out).toContain("0 fail"); // If `.resolves` blocks, each test runs to completion before the // next starts and maxInFlight stays at 1. With the deferred path - // all ten are in flight at once. - expect(out).toContain("MAX_INFLIGHT=10"); + // the tests overlap up to the runner's concurrency cap (5 on ASAN, + // 20 otherwise). + const maxInFlight = Number(out.match(/MAX_INFLIGHT=(\d+)/)?.[1]); + expect(maxInFlight).toBeGreaterThan(1); expect(exitCode).toBe(0); }, 40_000); }); From 5a0f112f8509d5230b0ee5042aa25ad110214cf0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 23:04:26 +0000 Subject: [PATCH 15/26] Address review: drop dead deferred_result; fix double-GC in apply_custom_matcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Expect::deferred_result() — the Rust port returns the deferred promise directly via MaybeDeferred::Deferred and the ready_*! macros, so the Zig-style resultValue slot read is unused. - apply_custom_matcher: move the pre-existing auto_gc_on_drop() into the static-call branch only; the dynamic branch's post_match_guard() already runs auto_garbage_collect() via post_match(), so keeping both fired it twice per custom-matcher call. - bake-codegen.ts: wrap the OVERLAY_CSS define value in JSON.stringify so it's a valid JS expression. Bun.build's define parser now rejects raw CSS as a value ("Operators are not allowed in JSON"); this was a latent bug exposed by the 1.3.14 host compiler, unrelated to this PR but blocks the build. --- src/codegen/bake-codegen.ts | 3 ++- src/runtime/test_runner/expect.rs | 12 ++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/codegen/bake-codegen.ts b/src/codegen/bake-codegen.ts index b60a3dba613..d001f4b766d 100644 --- a/src/codegen/bake-codegen.ts +++ b/src/codegen/bake-codegen.ts @@ -37,7 +37,8 @@ function css(file: string, is_development: boolean): string { stdio: ["ignore", "pipe", "pipe"], }); if (!success) throw new Error(stderr.toString("utf-8")); - return stdout.toString("utf-8"); + // `define` values must be valid JS expressions; wrap as a string literal. + return JSON.stringify(stdout.toString("utf-8")); } async function run() { diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index b4e476e2d9a..32f21a7dba0 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -438,13 +438,6 @@ impl Expect { .map(MaybeDeferred::Value) } - /// Returns the promise that a deferred matcher should return to its - /// caller. Only valid when `get_value()` returned - /// `MaybeDeferred::Deferred`. - pub fn deferred_result(&self, this_value: JSValue) -> JSValue { - super::expect::js::result_value_get_cached(this_value).unwrap_or(JSValue::UNDEFINED) - } - /// If `.resolves`/`.rejects` is set and `value` is a still-pending /// promise, sets up a `.then()` callback to re-invoke the current matcher /// once the promise settles, stores the returned-to-caller promise in the @@ -1646,9 +1639,6 @@ impl Expect { /// and we can known which case it is based on if the `callFrame.this()` value is an instance of Expect // extern shim emitted by `#[bun_jsc::JsClass]` codegen (TypeClass__construct/__call); bare `#[host_fn]` cannot target an associated fn without a receiver. pub fn apply_custom_matcher(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - // SAFETY: bun_vm() returns the live VM pointer for this global. - let _gc = global_this.bun_vm().as_mut().auto_gc_on_drop(); - // retrieve the user-provided matcher function (matcher_fn) let func: JSValue = call_frame.callee(); let matcher_fn: JSValue = get_custom_matcher_fn(func, global_this).unwrap_or(JSValue::UNDEFINED); @@ -1664,6 +1654,8 @@ impl Expect { let this_value: JSValue = call_frame.this(); let Some(expect_ptr) = Expect::from_js(this_value) else { // if no Expect instance, assume it is a static call (`expect.myMatcher()`), so create an ExpectCustomAsymmetricMatcher instance + // SAFETY: bun_vm() returns the live VM pointer for this global. + let _gc = global_this.bun_vm().as_mut().auto_gc_on_drop(); return ExpectCustomAsymmetricMatcher::create(global_this, call_frame, matcher_fn); }; // SAFETY: from_js returned a non-null live m_ctx pointer owned by the JS wrapper. From 905160e4e6eec96785bfddbb32289dc1dbb20372 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 23:14:01 +0000 Subject: [PATCH 16/26] Drop dead result_value_set_cached write in maybe_defer_matcher The deferred promise is returned directly via MaybeDeferred::Deferred and rooted by PendingMatcher.deferred: JSPromiseStrong; the resultValue cached slot is no longer read (its only reader was deferred_result(), removed in ee7f7108). --- src/runtime/test_runner/expect.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 32f21a7dba0..3934a94a3af 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -476,7 +476,6 @@ impl Expect { self.counted_expect_call.get(), self.flags.get(), )?; - super::expect::js::result_value_set_cached(this_value, global_this, deferred); Ok(Some(deferred)) } From 03f38b7406bf4595b2f6e325166cd1d994c9a6df Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 23:40:50 +0000 Subject: [PATCH 17/26] Use bun_core::heap::{into_raw, take} per repo convention Matches the Bun__TestScope__Describe2__* pattern this code cites as its model. Zero functional change. --- src/runtime/test_runner/expect.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 3934a94a3af..40398480bc8 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1965,7 +1965,7 @@ impl PendingMatcher { promise_value.then( global_this, - Box::into_raw(pending), + bun_core::heap::into_raw(pending), Bun__Expect__PendingMatcher__onResolve, Bun__Expect__PendingMatcher__onReject, ); @@ -1979,11 +1979,11 @@ impl PendingMatcher { if ctx.is_empty_or_undefined_or_null() { return Ok(JSValue::UNDEFINED); } - // SAFETY: `ctx` was produced by `Box::into_raw` in `create()` and is + // SAFETY: `ctx` was produced by `heap::into_raw` in `create()` and is // consumed exactly once here (onResolve and onReject are mutually // exclusive for a given promise settlement). let mut this = - unsafe { Box::from_raw(ctx.as_ptr_address() as *mut PendingMatcher) }; + unsafe { bun_core::heap::take(ctx.as_ptr_address() as *mut PendingMatcher) }; this.settle(global_this); // `Box` drop releases all `Strong` refs and the srcloc string. Ok(JSValue::UNDEFINED) From e090e7598a3316fea28c20ffabc0127c409fb33c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 15 May 2026 00:29:16 +0000 Subject: [PATCH 18/26] Fix stale doc comment; await .resolves in inspector-protocol websocket tests - maybe_defer_matcher doc still referenced the removed resultValue slot write; drop that clause. - packages/bun-inspector-protocol/test/inspector/websocket.test.ts had 13 un-awaited expect(...).resolves.() calls in sync test bodies. These relied on the old blocking waitForPromise behavior and would race under the deferred path. Make the tests async and await each assertion. --- .../test/inspector/websocket.test.ts | 46 +++++++++---------- src/runtime/test_runner/expect.rs | 4 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/bun-inspector-protocol/test/inspector/websocket.test.ts b/packages/bun-inspector-protocol/test/inspector/websocket.test.ts index 2514728cbc3..88f6eb9c7ea 100644 --- a/packages/bun-inspector-protocol/test/inspector/websocket.test.ts +++ b/packages/bun-inspector-protocol/test/inspector/websocket.test.ts @@ -7,69 +7,69 @@ let server: Server; let url: URL; describe("WebSocketInspector", () => { - test("fails without a URL", () => { + test("fails without a URL", async () => { const ws = new WebSocketInspector(); const fn = mock(error => { expect(error).toBeInstanceOf(Error); }); ws.on("Inspector.error", fn); - expect(ws.start()).resolves.toBeFalse(); + await expect(ws.start()).resolves.toBeFalse(); expect(fn).toHaveBeenCalled(); }); - test("fails with invalid URL", () => { + test("fails with invalid URL", async () => { const ws = new WebSocketInspector("notaurl"); const fn = mock(error => { expect(error).toBeInstanceOf(Error); }); ws.on("Inspector.error", fn); - expect(ws.start()).resolves.toBeFalse(); + await expect(ws.start()).resolves.toBeFalse(); expect(fn).toHaveBeenCalled(); }); - test("fails with valid URL but no server", () => { + test("fails with valid URL but no server", async () => { const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/"); const fn = mock(error => { expect(error).toBeInstanceOf(Error); }); ws.on("Inspector.error", fn); - expect(ws.start()).resolves.toBeFalse(); + await expect(ws.start()).resolves.toBeFalse(); expect(fn).toHaveBeenCalled(); }); - test("fails with invalid upgrade response", () => { + test("fails with invalid upgrade response", async () => { const ws = new WebSocketInspector(new URL("/", url)); const fn = mock(error => { expect(error).toBeInstanceOf(Error); }); ws.on("Inspector.error", fn); - expect(ws.start()).resolves.toBeFalse(); + await expect(ws.start()).resolves.toBeFalse(); expect(fn).toHaveBeenCalled(); }); - test("can connect to a server", () => { + test("can connect to a server", async () => { const ws = new WebSocketInspector(url); const fn = mock(() => { expect(ws.closed).toBe(false); }); ws.on("Inspector.connected", fn); - expect(ws.start()).resolves.toBeTrue(); + await expect(ws.start()).resolves.toBeTrue(); expect(fn).toHaveBeenCalled(); ws.close(); }); - test("can disconnect from a server", () => { + test("can disconnect from a server", async () => { const ws = new WebSocketInspector(url); const fn = mock(() => { expect(ws.closed).toBeTrue(); }); ws.on("Inspector.disconnected", fn); - expect(ws.start()).resolves.toBeTrue(); + await expect(ws.start()).resolves.toBeTrue(); ws.close(); expect(fn).toHaveBeenCalled(); }); - test("can connect to a server multiple times", () => { + test("can connect to a server multiple times", async () => { const ws = new WebSocketInspector(url); const fn0 = mock(() => { expect(ws.closed).toBeFalse(); @@ -80,14 +80,14 @@ describe("WebSocketInspector", () => { }); ws.on("Inspector.disconnected", fn1); for (let i = 0; i < 3; i++) { - expect(ws.start()).resolves.toBeTrue(); + await expect(ws.start()).resolves.toBeTrue(); ws.close(); } expect(fn0).toHaveBeenCalledTimes(3); expect(fn1).toHaveBeenCalledTimes(3); }); - test("can send a request", () => { + test("can send a request", async () => { const ws = new WebSocketInspector(url); const fn0 = mock(request => { expect(request).toStrictEqual({ @@ -108,14 +108,14 @@ describe("WebSocketInspector", () => { }); }); ws.on("Inspector.response", fn1); - expect(ws.start()).resolves.toBeTrue(); - expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true }); + await expect(ws.start()).resolves.toBeTrue(); + await expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true }); expect(fn0).toHaveBeenCalled(); expect(fn1).toHaveBeenCalled(); ws.close(); }); - test("can send a request before connecting", () => { + test("can send a request before connecting", async () => { const ws = new WebSocketInspector(url); const fn0 = mock(request => { expect(request).toStrictEqual({ @@ -136,14 +136,14 @@ describe("WebSocketInspector", () => { }); ws.on("Inspector.response", fn1); const request = ws.send("Runtime.enable"); - expect(ws.start()).resolves.toBe(true); - expect(request).resolves.toMatchObject({ ok: true }); + await expect(ws.start()).resolves.toBe(true); + await expect(request).resolves.toMatchObject({ ok: true }); expect(fn0).toHaveBeenCalledTimes(2); expect(fn1).toHaveBeenCalled(); ws.close(); }); - test("can receive an event", () => { + test("can receive an event", async () => { const ws = new WebSocketInspector(url); const fn = mock(event => { expect(event).toStrictEqual({ @@ -154,8 +154,8 @@ describe("WebSocketInspector", () => { }); }); ws.on("Inspector.event", fn); - expect(ws.start()).resolves.toBeTrue(); - expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true }); + await expect(ws.start()).resolves.toBeTrue(); + await expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true }); expect(fn).toHaveBeenCalled(); ws.close(); }); diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 40398480bc8..e17ab69f453 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -440,8 +440,8 @@ impl Expect { /// If `.resolves`/`.rejects` is set and `value` is a still-pending /// promise, sets up a `.then()` callback to re-invoke the current matcher - /// once the promise settles, stores the returned-to-caller promise in the - /// `resultValue` slot, and returns it. Returns `None` otherwise. + /// once the promise settles, and returns the deferred-result promise for + /// the caller to await. Returns `None` otherwise. /// /// This replaces the old synchronous `wait_for_promise()` which would /// hang forever if the promise could only be resolved by JavaScript From a7193a1ee72550a3555298e367d689c8d2f8bf6a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 15 May 2026 01:06:39 +0000 Subject: [PATCH 19/26] ci: retrigger (alpine mysql_native_password Docker health-check flake) From bce8920b134234f8c0b8efe80d8da6ef0d840eb2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 16 May 2026 11:44:11 +0000 Subject: [PATCH 20/26] Revert bake-codegen.ts JSON.stringify workaround (obsoleted by #30679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSON.stringify() wrap around css() output was a local workaround for the host bun's define-value parser choking on raw '*{...}' CSS. #30679 fixed the JSON lexer to tokenize ?/*/(/) without erroring so parse_env_json's auto-quote fallback can recover — and that fix is now in the rebase base. Reverting restores bake-codegen as the in-tree exerciser of that path and un-stales the two comments that cite it (json_lexer.rs:1304 and bun-build-api.test.ts:76). --- src/codegen/bake-codegen.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/codegen/bake-codegen.ts b/src/codegen/bake-codegen.ts index d001f4b766d..b60a3dba613 100644 --- a/src/codegen/bake-codegen.ts +++ b/src/codegen/bake-codegen.ts @@ -37,8 +37,7 @@ function css(file: string, is_development: boolean): string { stdio: ["ignore", "pipe", "pipe"], }); if (!success) throw new Error(stderr.toString("utf-8")); - // `define` values must be valid JS expressions; wrap as a string literal. - return JSON.stringify(stdout.toString("utf-8")); + return stdout.toString("utf-8"); } async function run() { From 614a0f6df6537fe95d5239b836a51e1e27a3cfee Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 16 May 2026 12:04:48 +0000 Subject: [PATCH 21/26] Update matcher_prelude/mock_prologue doc comments for MatcherStart/MockStart return types Both docs still described the old tuple return shapes and direct destructuring. 45a36dbb changed the signatures to return MatcherStart<'a>/MockStart<'a> enums so the Deferred(promise) path can early-return; callers now unwrap via ready_matcher!/ready_mock!. Align the docs with the actual signatures and mention the deferred branch. --- src/runtime/test_runner/expect.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index e17ab69f453..33a336a3fa4 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1839,9 +1839,11 @@ impl Expect { /// [`Self::mock_prologue`], for matchers that need the received value but /// are NOT a pure unary predicate and NOT a mock-function matcher. /// - /// Returns `(guard, received_value, not)`. The guard derefs to `&Expect` - /// and runs `post_match` on drop; `not` is `flags.not()` snapshotted once. - /// Callers that don't need `not` until later destructure as `(this, v, _)`. + /// Returns [`MatcherStart::Ready(guard, received_value, not)`] in the + /// common case — the guard derefs to `&Expect` and runs `post_match` on + /// drop, and `not` is `flags.not()` snapshotted once — or + /// [`MatcherStart::Deferred`] when `.resolves`/`.rejects` was used on a + /// still-pending promise. Callers unwrap with [`ready_matcher!`]. #[inline] pub fn matcher_prelude<'a>( &'a self, @@ -3362,9 +3364,12 @@ pub mod mock { /// `.rejects`), bumps the assertion counter, fetches the requested /// mock-backed array, and emits the kind-appropriate "not a mock" error. /// - /// Returns the [`PostMatchGuard`] (so `post_match` runs when the caller - /// drops it), the `mock.calls` / `mock.results` JSArray, and the raw - /// received value (some matchers print it again on later error paths). + /// Returns [`MockStart::Ready(guard, arr, value)`] — the + /// [`PostMatchGuard`] (so `post_match` runs when the caller drops it), + /// the `mock.calls` / `mock.results` JSArray, and the raw received + /// value (some matchers print it again on later error paths) — or + /// [`MockStart::Deferred`] when `.resolves`/`.rejects` was used on a + /// still-pending promise. Callers unwrap with [`ready_mock!`]. pub fn mock_prologue<'a>( &'a self, global: &'a JSGlobalObject, From 43128b59908b36587249b7dd698ec0c27d896622 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 21 May 2026 08:25:43 +0000 Subject: [PATCH 22/26] Await deferred .resolves in pidfd-exit-nested-tick test to avoid LSan leak The test fires expect(Bun.sleep(1)).resolves.toBe(undefined) from a sync onExit callback without awaiting it. Under the deferred-matcher path that allocates a PendingMatcher Box and attaches it via .then() to the sleep promise. On release-asan CI the process exits before the 1ms timer fires, so on_settle() never runs and the 72-byte Box leaks, tripping LSan (surfaced by #31029's suppression cleanup). Capture the deferred promise and await it after Promise.all(exits) so the PendingMatcher is freed before process exit regardless of timing. The assertion is now actually checked too (1 expect() call instead of 0 on the fast path). The original purpose of this line was to force a synchronous nested event-loop tick via waitForPromise; that no longer applies under the deferred path (already noted in the preceding comment), and the test still validates the level-triggered pidfd fix via the all-exits-fire check. --- test/js/bun/spawn/pidfd-exit-nested-tick.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts index 0730ce4e15c..1a5bb2e2cdc 100644 --- a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts +++ b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts @@ -34,6 +34,7 @@ test.skipIf(!isLinux)( const N = 20; const exits: Array> = []; let nested = false; + let deferred: unknown; for (let i = 0; i < N; i++) { const { promise, resolve } = Promise.withResolvers(); @@ -58,10 +59,12 @@ test.skipIf(!isLinux)( // the synchronous nested tick that originally triggered the bug. // The test still validates that every child fires onExit (the // level-triggered pidfd registration makes that robust regardless - // of whether the nested-tick drop path is exercised). + // of whether the nested-tick drop path is exercised). The deferred + // matcher is awaited below so its PendingMatcher is freed before + // LSan checks at process exit. if (!nested) { nested = true; - expect(Bun.sleep(1)).resolves.toBe(undefined); + deferred = expect(Bun.sleep(1)).resolves.toBe(undefined); } resolve(); }, @@ -72,5 +75,6 @@ test.skipIf(!isLinux)( // whose events were dropped never fire onExit and this await hangs until // the test's own 5s timeout — there is no other wake source. await Promise.all(exits); + await deferred; }, ); From c6ed4f1efac1b4cda2ff5e5245d03ea13d5247e8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 21 May 2026 09:02:14 +0000 Subject: [PATCH 23/26] Fix SAFETY comment in RerunGuard::drop to reference the correct owner The comment said 'self.expect_this: Strong', but self here is &mut RerunGuard which has no such field. The Strong that keeps the Expect wrapper alive is on the enclosing PendingMatcher, owned by the Box in on_settle() which outlives _guard. Safety argument is unchanged; only the field attribution was wrong. --- src/runtime/test_runner/expect.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 33a336a3fa4..e49bfd931f8 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -2041,8 +2041,9 @@ impl PendingMatcher { impl Drop for RerunGuard { fn drop(&mut self) { // SAFETY: `expect` came from `Expect::from_js` on a value - // held live by `self.expect_this: Strong` for the duration - // of this scope. + // held live by the enclosing `PendingMatcher.expect_this: + // Strong` for the duration of this scope (the Box in + // `on_settle()` outlives `_guard`). unsafe { (*self.expect).is_async_rerun.set(self.saved_is_rerun); (*self.expect).flags.set(self.saved_flags); From 28a04ee97a85e87530ffe8ce93babe69f026148f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 07:10:41 +0000 Subject: [PATCH 24/26] Store deferred-matcher state in a GC-managed context instead of a Box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PendingMatcher Box (with Strong roots for the Expect wrapper, matcher fn, args array, and deferred promise) was only freed in on_settle(). If the input promise never settled before process exit — e.g. an un-awaited expect(pending).resolves... whose promise races teardown, as vendor/elysia's router.test.ts does — the Box leaked and LeakSanitizer aborted the ASAN lane. Keep all per-deferral state in a 9-slot JS array passed as the promise reaction's context via then_with_value, and make the deferred promise a plain GC JSPromise rather than a Strong. The reaction traces the array, so a never-settling promise takes the whole bundle with it when it is collected: nothing native outlives the reaction and there is nothing left for LSan to report. on_settle() now unpacks the array (flags/counted bits and srcloc round-trip as number/boolean/string slots) and rerun() takes the unpacked values; behaviour on the settle path is unchanged. --- src/runtime/test_runner/expect.rs | 216 +++++++++++++++++++----------- 1 file changed, 135 insertions(+), 81 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index e49bfd931f8..db6d36f2e9e 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1909,37 +1909,48 @@ impl Drop for PostMatchGuard<'_> { /// State for a `.resolves`/`.rejects` matcher call whose promise hasn't /// settled yet. When it does, we re-invoke the matcher and resolve/reject -/// `deferred` with the outcome. -pub struct PendingMatcher { +/// the deferred promise with the outcome. +/// +/// The state is stored in a GC-managed JS array passed as the promise +/// reaction's context (`then_with_value`), NOT in a native allocation: +/// if the input promise never settles (e.g. an un-awaited +/// `expect(new Promise(()=>{})).resolves...`), the reaction — and with it +/// this array and everything it references — is simply garbage-collected. +/// Nothing leaks and LeakSanitizer stays quiet. +pub struct PendingMatcher; + +impl PendingMatcher { + /// Slots of the context array built in [`Self::create`]. /// The `Expect` JS instance (also keeps capturedValue alive). - expect_this: bun_jsc::Strong, + const SLOT_EXPECT_THIS: u32 = 0; /// The native matcher function being called (e.g. `toBe`). - matcher_fn: bun_jsc::Strong, + const SLOT_MATCHER_FN: u32 = 1; /// JSArray of the arguments the matcher was called with. - matcher_args: bun_jsc::Strong, - /// The promise returned to the caller of the matcher. - deferred: bun_jsc::JSPromiseStrong, + const SLOT_MATCHER_ARGS: u32 = 2; + /// The pending promise returned to the caller of the matcher. + const SLOT_DEFERRED: u32 = 3; /// Whether `increment_expect_call_counter()` had already run by the - /// time we deferred. Restored on re-run so the counter is bumped - /// exactly once regardless of whether this matcher calls it before or - /// after `get_value()`. - was_counted_before_defer: bool, - /// The `Expect.flags` at the time we deferred. `.not` / `.resolves` / - /// `.rejects` mutate flags in place on the shared instance, so a later - /// matcher call on the same `expect()` could flip them before this - /// re-run fires; restore so the re-run observes the flags the user - /// wrote for *this* matcher call. - flags_at_defer: Flags, - /// The caller's source location at the time we deferred, captured - /// while the user's frame is still on the stack. `inline_snapshot()` + /// time we deferred (boolean). Restored on re-run so the counter is + /// bumped exactly once regardless of whether this matcher calls it + /// before or after `get_value()`. + const SLOT_WAS_COUNTED: u32 = 4; + /// The `Expect.flags` bits at the time we deferred (number). `.not` / + /// `.resolves` / `.rejects` mutate flags in place on the shared + /// instance, so a later matcher call on the same `expect()` could flip + /// them before this re-run fires; restore so the re-run observes the + /// flags the user wrote for *this* matcher call. + const SLOT_FLAGS: u32 = 5; + /// Caller source location captured while the user's frame is still on + /// the stack, split into string/line/column slots. `inline_snapshot()` /// needs it on the re-run to know which line to write the snapshot - /// back to. Stored per-PendingMatcher (not on the shared `Expect`) so - /// two deferred inline-snapshot matchers on the same `expect()` - /// instance each write to their own call site. - srcloc_at_defer: bun_jsc::CallerSrcLoc, -} + /// back to. Stored per-deferral (not on the shared `Expect`) so two + /// deferred inline-snapshot matchers on the same `expect()` instance + /// each write to their own call site. + const SLOT_SRCLOC_STR: u32 = 6; + const SLOT_SRCLOC_LINE: u32 = 7; + const SLOT_SRCLOC_COLUMN: u32 = 8; + const SLOT_COUNT: usize = 9; -impl PendingMatcher { fn create( global_this: &JSGlobalObject, this_value: JSValue, @@ -1954,20 +1965,41 @@ impl PendingMatcher { args_array.put_index(global_this, i as u32, *arg)?; } - let pending = Box::new(PendingMatcher { - expect_this: bun_jsc::Strong::create(this_value, global_this), - matcher_fn: bun_jsc::Strong::create(callframe.callee(), global_this), - matcher_args: bun_jsc::Strong::create(args_array, global_this), - deferred: bun_jsc::JSPromiseStrong::init(global_this), - was_counted_before_defer: was_counted, - flags_at_defer: flags, - srcloc_at_defer: callframe.get_caller_src_loc(global_this), - }); - let deferred_value = pending.deferred.value(); + let deferred_value = js_promise::JSPromise::create(global_this).to_js(); + + // `get_caller_src_loc` returns the path with a +1 ref; convert it to + // a JS string for GC-managed storage and release the native ref on + // scope exit. + let srcloc = callframe.get_caller_src_loc(global_this); + let srcloc_str_guard = bun_core::OwnedString::new(srcloc.str); + let srcloc_str_js = srcloc_str_guard.get().to_js(global_this)?; + + let ctx = JSValue::create_empty_array(global_this, Self::SLOT_COUNT)?; + ctx.put_index(global_this, Self::SLOT_EXPECT_THIS, this_value)?; + ctx.put_index(global_this, Self::SLOT_MATCHER_FN, callframe.callee())?; + ctx.put_index(global_this, Self::SLOT_MATCHER_ARGS, args_array)?; + ctx.put_index(global_this, Self::SLOT_DEFERRED, deferred_value)?; + ctx.put_index(global_this, Self::SLOT_WAS_COUNTED, JSValue::js_boolean(was_counted))?; + ctx.put_index( + global_this, + Self::SLOT_FLAGS, + JSValue::js_number_from_int32(flags.0 as i32), + )?; + ctx.put_index(global_this, Self::SLOT_SRCLOC_STR, srcloc_str_js)?; + ctx.put_index( + global_this, + Self::SLOT_SRCLOC_LINE, + JSValue::js_number_from_int32(srcloc.line as i32), + )?; + ctx.put_index( + global_this, + Self::SLOT_SRCLOC_COLUMN, + JSValue::js_number_from_int32(srcloc.column as i32), + )?; - promise_value.then( + promise_value.then_with_value( global_this, - bun_core::heap::into_raw(pending), + ctx, Bun__Expect__PendingMatcher__onResolve, Bun__Expect__PendingMatcher__onReject, ); @@ -1978,42 +2010,73 @@ impl PendingMatcher { fn on_settle(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let args = callframe.arguments_old::<2>(); let ctx = args.ptr[1]; - if ctx.is_empty_or_undefined_or_null() { + if ctx.is_empty_or_undefined_or_null() || !ctx.is_cell() { return Ok(JSValue::UNDEFINED); } - // SAFETY: `ctx` was produced by `heap::into_raw` in `create()` and is - // consumed exactly once here (onResolve and onReject are mutually - // exclusive for a given promise settlement). - let mut this = - unsafe { bun_core::heap::take(ctx.as_ptr_address() as *mut PendingMatcher) }; - this.settle(global_this); - // `Box` drop releases all `Strong` refs and the srcloc string. - Ok(JSValue::UNDEFINED) - } - fn settle(&mut self, global_this: &JSGlobalObject) { - match self.rerun(global_this) { - Ok(result) => { - let _ = self.deferred.resolve(global_this, result); - } - Err(_) => { - let exception = global_this - .try_take_exception() - .unwrap_or(JSValue::UNDEFINED); - let _ = self.deferred.reject(global_this, Ok(exception)); + let expect_this = ctx.get_index(global_this, Self::SLOT_EXPECT_THIS)?; + let matcher_fn = ctx.get_index(global_this, Self::SLOT_MATCHER_FN)?; + let args_array = ctx.get_index(global_this, Self::SLOT_MATCHER_ARGS)?; + let deferred_value = ctx.get_index(global_this, Self::SLOT_DEFERRED)?; + let was_counted = ctx.get_index(global_this, Self::SLOT_WAS_COUNTED)?.to_boolean(); + let flags = Flags(ctx.get_index(global_this, Self::SLOT_FLAGS)?.to_int32() as u8); + + // Rebuild the caller srcloc. The bun String is owned here (+1 from + // `to_bun_string`) and released when `_srcloc_str_guard` drops, after + // the re-run below has finished using it via `async_rerun_srcloc`. + let srcloc_str_js = ctx.get_index(global_this, Self::SLOT_SRCLOC_STR)?; + let _srcloc_str_guard = + bun_core::OwnedString::new(srcloc_str_js.to_bun_string(global_this)?); + let srcloc = bun_jsc::CallerSrcLoc { + str: _srcloc_str_guard.get(), + line: ctx.get_index(global_this, Self::SLOT_SRCLOC_LINE)?.to_int32() as core::ffi::c_uint, + column: ctx.get_index(global_this, Self::SLOT_SRCLOC_COLUMN)?.to_int32() + as core::ffi::c_uint, + }; + + let result = Self::rerun( + global_this, + expect_this, + matcher_fn, + args_array, + was_counted, + flags, + srcloc, + ); + + if let Some(deferred) = deferred_value.as_promise() { + // SAFETY: `deferred_value` was created by `JSPromise::create` in + // `create()` and is kept alive by the ctx array (rooted by this + // frame's arguments) for the duration of this call. + let deferred = unsafe { &mut *deferred }; + match result { + Ok(value) => { + let _ = deferred.resolve(global_this, value); + } + Err(_) => { + let exception = global_this + .try_take_exception() + .unwrap_or(JSValue::UNDEFINED); + let _ = deferred.reject(global_this, Ok(exception)); + } } } + Ok(JSValue::UNDEFINED) } - fn rerun(&mut self, global_this: &JSGlobalObject) -> JsResult { - let expect_this = self.expect_this.get(); - let matcher_fn = self.matcher_fn.get(); - let args_array = self.matcher_args.get(); - + #[allow(clippy::too_many_arguments)] + fn rerun( + global_this: &JSGlobalObject, + expect_this: JSValue, + matcher_fn: JSValue, + args_array: JSValue, + was_counted: bool, + flags: Flags, + srcloc: bun_jsc::CallerSrcLoc, + ) -> JsResult { // Extract the original arguments back out of the array. Most // built-in matchers take 0-2 arguments, but `toHaveBeenCalledWith` // and `expect.extend()` custom matchers are variadic. - // PERF(port): was stack-fallback allocator — profile in Phase B let args_len = args_array.get_length(global_this)? as usize; let mut args_buf: Vec = Vec::with_capacity(args_len); for i in 0..args_len { @@ -2025,7 +2088,7 @@ impl PendingMatcher { // its result synchronously. // // `is_async_rerun`, `flags`, and `async_rerun_srcloc` are - // saved/restored (not hard-reset) because a sibling `PendingMatcher` + // saved/restored (not hard-reset) because a sibling deferred matcher // on the same input promise can rerun *inside* `matcher_fn.call()` // when the matcher re-enters the event loop (e.g. `toThrow` → // `get_value_as_to_throw` → `wait_for_promise` → `tick` drains the @@ -2041,9 +2104,8 @@ impl PendingMatcher { impl Drop for RerunGuard { fn drop(&mut self) { // SAFETY: `expect` came from `Expect::from_js` on a value - // held live by the enclosing `PendingMatcher.expect_this: - // Strong` for the duration of this scope (the Box in - // `on_settle()` outlives `_guard`). + // held live by the ctx array in `on_settle()` (rooted by + // that frame's arguments) for the duration of this scope. unsafe { (*self.expect).is_async_rerun.set(self.saved_is_rerun); (*self.expect).flags.set(self.saved_flags); @@ -2052,7 +2114,7 @@ impl PendingMatcher { } } let _guard = Expect::from_js(expect_this).map(|expect| { - // SAFETY: `expect_this` is held live by `self.expect_this`. + // SAFETY: `expect_this` is held live by the ctx array in `on_settle()`. let e = unsafe { &*expect }; let guard = RerunGuard { expect, @@ -2061,9 +2123,9 @@ impl PendingMatcher { saved_srcloc: e.async_rerun_srcloc.get(), }; e.is_async_rerun.set(true); - e.counted_expect_call.set(self.was_counted_before_defer); - e.flags.set(self.flags_at_defer); - e.async_rerun_srcloc.set(Some(self.srcloc_at_defer)); + e.counted_expect_call.set(was_counted); + e.flags.set(flags); + e.async_rerun_srcloc.set(Some(srcloc)); guard }); @@ -2071,18 +2133,10 @@ impl PendingMatcher { } } -impl Drop for PendingMatcher { - fn drop(&mut self) { - // Release the +1 on the captured srcloc string. - self.srcloc_at_defer.str.deref(); - // `Strong` / `JSPromiseStrong` fields release via their own Drop. - } -} - // `ZigGlobalObject::promiseHandlerID` (C++) compares the fn-ptr passed to // `JSValue::then` against these by identity, so the Rust thunk MUST be the -// exported symbol itself — see the PORT NOTE on `Bun__TestScope__*` in -// `bun_test.rs`. +// exported symbol itself — see the comment on `Bun__TestScope__Describe2__*` +// in `bun_test.rs`. bun_jsc::jsc_host_abi! { #[unsafe(no_mangle)] pub unsafe fn Bun__Expect__PendingMatcher__onResolve( From 9959864b26c679dbb22ca86f2d6a08928dedd41f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:08:15 +0000 Subject: [PATCH 25/26] Await maxRedirects rejects assertion; fix two stale deferred-matcher comments - test/js/web/fetch/fetch.test.ts: the maxRedirects test added by #31518 (post-sweep, came in via rebase) fired expect(fetch()).rejects.toThrow without awaiting; under the deferred path the assertion would never be checked within the test. Same one-line await as the rest of the file. - expect.rs: async_rerun_srcloc doc referenced the removed PendingMatcher.srcloc_at_defer field; it now describes the SLOT_SRCLOC_* context-array slots and on_settle() ownership. - pidfd-exit-nested-tick.test.ts: drop the stale LSan rationale (nothing native is allocated since the GC-managed-context rewrite); the await is about completing the assertion within the test. --- src/runtime/test_runner/expect.rs | 11 ++++++----- test/js/bun/spawn/pidfd-exit-nested-tick.test.ts | 4 ++-- test/js/web/fetch/fetch.test.ts | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index db6d36f2e9e..881c6a3ca48 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -49,11 +49,12 @@ pub struct Expect { /// it at defer time and restores it before the re-run. pub counted_expect_call: Cell, /// The caller's source location for the currently-executing deferred - /// re-run, borrowed from `PendingMatcher.srcloc_at_defer`. Written by - /// `PendingMatcher::rerun()` for the duration of the re-invoked matcher - /// call and restored afterwards. `inline_snapshot()` reads it (gated on - /// `is_async_rerun`) because the user's frame is no longer on the stack. - /// Owned by the `PendingMatcher`, not by `Expect`. + /// re-run, reconstructed from the context array's `SLOT_SRCLOC_*` slots. + /// Written by `PendingMatcher::rerun()` for the duration of the + /// re-invoked matcher call and restored afterwards. `inline_snapshot()` + /// reads it (gated on `is_async_rerun`) because the user's frame is no + /// longer on the stack. The string is owned by `on_settle()`'s scope, + /// not by `Expect`. pub async_rerun_srcloc: Cell>, } diff --git a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts index 1a5bb2e2cdc..193c6cb9d68 100644 --- a/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts +++ b/test/js/bun/spawn/pidfd-exit-nested-tick.test.ts @@ -60,8 +60,8 @@ test.skipIf(!isLinux)( // The test still validates that every child fires onExit (the // level-triggered pidfd registration makes that robust regardless // of whether the nested-tick drop path is exercised). The deferred - // matcher is awaited below so its PendingMatcher is freed before - // LSan checks at process exit. + // matcher is awaited below so the assertion completes within this + // test rather than racing process teardown. if (!nested) { nested = true; deferred = expect(Bun.sleep(1)).resolves.toBe(undefined); diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index c52cbaf59dd..37ea04b8696 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -1947,7 +1947,7 @@ describe("maxRedirects", () => { }); it("rejects once the chain exceeds maxRedirects", async () => { - expect(fetch(`${server.url}hop/0`, { maxRedirects: 2 })).rejects.toThrow("redirected too many times"); + await expect(fetch(`${server.url}hop/0`, { maxRedirects: 2 })).rejects.toThrow("redirected too many times"); }); it("follows the chain when maxRedirects is large enough", async () => { From 5c06bfd3dc9ec68f28f586896522c5edbbd739ec Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:52:08 +0000 Subject: [PATCH 26/26] Settle the deferred even when a context slot read fails in on_settle Read SLOT_DEFERRED first and move the remaining fallible slot reads into rerun(), so any failure there feeds the deferred's reject arm instead of leaving it pending. Also fix a stale comment in inline_snapshot that referenced PendingMatcher owning the srcloc deref. --- src/runtime/test_runner/expect.rs | 73 ++++++++++++++----------------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 881c6a3ca48..7ec207ffed5 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -53,8 +53,8 @@ pub struct Expect { /// Written by `PendingMatcher::rerun()` for the duration of the /// re-invoked matcher call and restored afterwards. `inline_snapshot()` /// reads it (gated on `is_async_rerun`) because the user's frame is no - /// longer on the stack. The string is owned by `on_settle()`'s scope, - /// not by `Expect`. + /// longer on the stack. The string is owned by `rerun()`'s scope, not by + /// `Expect`. pub async_rerun_srcloc: Cell>, } @@ -1182,7 +1182,8 @@ impl Expect { // bun_core::String is Copy // with no Drop, so wrap in the RAII guard to release the +1 on // every exit path (including the early returns below). When the - // srcloc is borrowed from `PendingMatcher`, it owns the deref. + // srcloc is borrowed (the deferred-rerun path), + // `PendingMatcher::rerun()`'s scope owns the deref. let _srcloc_str_guard = owns_srcloc.then(|| bun_core::OwnedString::new(srcloc.str)); let file_id = buntest.file_id; // MultiArrayList::get requires MultiArrayElement (derive pending); @@ -2015,35 +2016,13 @@ impl PendingMatcher { return Ok(JSValue::UNDEFINED); } - let expect_this = ctx.get_index(global_this, Self::SLOT_EXPECT_THIS)?; - let matcher_fn = ctx.get_index(global_this, Self::SLOT_MATCHER_FN)?; - let args_array = ctx.get_index(global_this, Self::SLOT_MATCHER_ARGS)?; + // Read the deferred before anything else fallible: every remaining + // slot read and the re-run itself happen inside `rerun()`, whose + // error feeds the reject arm below instead of returning early and + // leaving the deferred pending forever. let deferred_value = ctx.get_index(global_this, Self::SLOT_DEFERRED)?; - let was_counted = ctx.get_index(global_this, Self::SLOT_WAS_COUNTED)?.to_boolean(); - let flags = Flags(ctx.get_index(global_this, Self::SLOT_FLAGS)?.to_int32() as u8); - - // Rebuild the caller srcloc. The bun String is owned here (+1 from - // `to_bun_string`) and released when `_srcloc_str_guard` drops, after - // the re-run below has finished using it via `async_rerun_srcloc`. - let srcloc_str_js = ctx.get_index(global_this, Self::SLOT_SRCLOC_STR)?; - let _srcloc_str_guard = - bun_core::OwnedString::new(srcloc_str_js.to_bun_string(global_this)?); - let srcloc = bun_jsc::CallerSrcLoc { - str: _srcloc_str_guard.get(), - line: ctx.get_index(global_this, Self::SLOT_SRCLOC_LINE)?.to_int32() as core::ffi::c_uint, - column: ctx.get_index(global_this, Self::SLOT_SRCLOC_COLUMN)?.to_int32() - as core::ffi::c_uint, - }; - let result = Self::rerun( - global_this, - expect_this, - matcher_fn, - args_array, - was_counted, - flags, - srcloc, - ); + let result = Self::rerun(global_this, ctx); if let Some(deferred) = deferred_value.as_promise() { // SAFETY: `deferred_value` was created by `JSPromise::create` in @@ -2065,16 +2044,30 @@ impl PendingMatcher { Ok(JSValue::UNDEFINED) } - #[allow(clippy::too_many_arguments)] - fn rerun( - global_this: &JSGlobalObject, - expect_this: JSValue, - matcher_fn: JSValue, - args_array: JSValue, - was_counted: bool, - flags: Flags, - srcloc: bun_jsc::CallerSrcLoc, - ) -> JsResult { + /// Reads the re-run inputs back out of the context array and re-invokes + /// the matcher. Fallible end to end: `on_settle()` routes any error from + /// here, including the slot reads, into the deferred's reject arm. + fn rerun(global_this: &JSGlobalObject, ctx: JSValue) -> JsResult { + let expect_this = ctx.get_index(global_this, Self::SLOT_EXPECT_THIS)?; + let matcher_fn = ctx.get_index(global_this, Self::SLOT_MATCHER_FN)?; + let args_array = ctx.get_index(global_this, Self::SLOT_MATCHER_ARGS)?; + let was_counted = ctx.get_index(global_this, Self::SLOT_WAS_COUNTED)?.to_boolean(); + let flags = Flags(ctx.get_index(global_this, Self::SLOT_FLAGS)?.to_int32() as u8); + + // Rebuild the caller srcloc. The bun String is owned here (+1 from + // `to_bun_string`) and released when `_srcloc_str_guard` drops, after + // the matcher call below has finished using it via + // `async_rerun_srcloc`. + let srcloc_str_js = ctx.get_index(global_this, Self::SLOT_SRCLOC_STR)?; + let _srcloc_str_guard = + bun_core::OwnedString::new(srcloc_str_js.to_bun_string(global_this)?); + let srcloc = bun_jsc::CallerSrcLoc { + str: _srcloc_str_guard.get(), + line: ctx.get_index(global_this, Self::SLOT_SRCLOC_LINE)?.to_int32() as core::ffi::c_uint, + column: ctx.get_index(global_this, Self::SLOT_SRCLOC_COLUMN)?.to_int32() + as core::ffi::c_uint, + }; + // Extract the original arguments back out of the array. Most // built-in matchers take 0-2 arguments, but `toHaveBeenCalledWith` // and `expect.extend()` custom matchers are variadic.