diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 9b9e0731c1..66be78d92d 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -380,6 +380,9 @@ public: if (auto* moduleNamespaceObject = tryJSDynamicCast(target)) { moduleNamespaceObject->overrideExportValue(moduleNamespaceObject->globalObject(), this->spyIdentifier, implValue); } + } else if (auto index = parseIndex(this->spyIdentifier)) { + // Use putDirectIndex for numeric property keys (e.g., spyOn(arr, 0)) + target->putDirectIndex(globalObject(), *index, implValue, this->spyAttributes, PutDirectIndexLikePutDirect); } else { target->putDirect(this->vm(), this->spyIdentifier, implValue, this->spyAttributes); } @@ -1528,6 +1531,9 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb if (JSModuleNamespaceObject* moduleNamespaceObject = tryJSDynamicCast(object)) { moduleNamespaceObject->overrideExportValue(globalObject, propertyKey, mock); mock->spyAttributes |= JSMockFunction::SpyAttributeESModuleNamespace; + } else if (auto index = parseIndex(propertyKey)) { + // Use putDirectIndex for numeric property keys (e.g., spyOn(arr, 0)) + object->putDirectIndex(globalObject, *index, mock, attributes, PutDirectIndexLikePutDirect); } else { object->putDirect(vm, propertyKey, mock, attributes); } @@ -1544,6 +1550,9 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb if (JSModuleNamespaceObject* moduleNamespaceObject = tryJSDynamicCast(object)) { moduleNamespaceObject->overrideExportValue(globalObject, propertyKey, mock); mock->spyAttributes |= JSMockFunction::SpyAttributeESModuleNamespace; + } else if (auto index = parseIndex(propertyKey)) { + // For indexed properties, set the mock directly instead of wrapping in GetterSetter + object->putDirectIndex(globalObject, *index, mock, attributes, PutDirectIndexLikePutDirect); } else { object->putDirectAccessor(globalObject, propertyKey, JSC::GetterSetter::create(vm, globalObject, mock, mock), attributes); } diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 128f1fa110..7f6a244d98 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -952,5 +952,66 @@ describe("spyOn", () => { expect(fn).not.toBe(_original); }); + if (isBun) { + // Test for spyOn with numeric/indexed property keys + test("spyOn works with indexed properties", () => { + function original() { + return 42; + } + const arr = []; + arr[0] = original; + + const fn = spyOn(arr, 0); + expect(fn).toBe(arr[0]); + expect(fn).not.toHaveBeenCalled(); + expect(arr[0]()).toBe(42); + expect(fn).toHaveBeenCalled(); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.calls).toHaveLength(1); + + fn.mockRestore(); + expect(arr[0]).toBe(original); + expect(arr[0]()).toBe(42); + expect(fn).not.toHaveBeenCalled(); + }); + + test("spyOn works with indexed properties using string keys", () => { + function original() { + return 123; + } + const arr = []; + arr[0] = original; + + // Using string "0" instead of number 0 + const fn = spyOn(arr, "0"); + expect(fn).toBe(arr[0]); + expect(arr[0]()).toBe(123); + expect(fn).toHaveBeenCalled(); + + fn.mockRestore(); + expect(arr[0]).toBe(original); + }); + + test("spyOn works with indexed properties using BigInt keys", () => { + function original() { + return 456; + } + const arr = []; + arr[14] = original; + + // Using BigInt 14n as property key + const fn = spyOn(arr, 14n); + expect(fn).toBe(arr[14]); + expect(arr[14]()).toBe(456); + expect(fn).toHaveBeenCalled(); + expect(fn).toHaveBeenCalledTimes(1); + + fn.mockRestore(); + expect(arr[14]).toBe(original); + expect(arr[14]()).toBe(456); + expect(fn).not.toHaveBeenCalled(); + }); + } + // spyOn does not work with getters/setters yet. });