diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 9b39827ad788..b7f8e623bf78 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -838,7 +838,9 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje } JSC::ArgList args = JSC::ArgList(callframe); - JSValue thisValue = callframe->thisValue(); + // For `fn()` calls JSC places the resolved scope (a JSScope) in the `this` slot and expects the + // callee to sanitize it. Do so before exposing it to JS, like ProxyObject::performCall does. + JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); JSC::JSArray* argumentsArray = nullptr; { JSC::ObjectInitializationScope object(vm); diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 7f6a244d9806..f84c295729c5 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -238,6 +238,20 @@ describe("mock()", () => { const obj = { fn }; expect(obj.fn()).toBe(obj); }); + test("bare call through a captured binding has an undefined this value", () => { + const fn = jest.fn().mockReturnThis(); + let returned; + { + const ref = fn; + // Capturing `ref` in a closure makes the engine resolve `ref()` through a + // scope object; that scope must not be observable as the call's `this`. + const capture = () => ref; + returned = ref(); + expect(capture()).toBe(fn); + } + expect(returned).toBeUndefined(); + expect(fn.mock.contexts).toEqual([undefined]); + }); if (isBun) { test("jest.fn(10) return value shorthand", () => { expect(jest.fn(10)()).toBe(10);