fix(test): use putDirectMayBeIndex in spyOn for indexed property keys (#25020)

## Summary
- Fix `spyOn` crash when using indexed property keys (e.g., `spyOn(arr,
0)`)

## Test plan
- [x] Added tests for `spyOn` with numeric indexed properties
- [x] Added tests for `spyOn` with string indexed properties (e.g.,
`"0"`)
- [x] All existing `spyOn` tests pass
- [x] Full `mock-fn.test.js` test suite passes

Fixes ENG-21973

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
robobun
2025-11-24 19:27:33 -08:00
committed by GitHub
parent 3f0681996f
commit cc393e43f2
2 changed files with 70 additions and 0 deletions

View File

@@ -380,6 +380,9 @@ public:
if (auto* moduleNamespaceObject = tryJSDynamicCast<JSModuleNamespaceObject*>(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<JSModuleNamespaceObject*>(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<JSModuleNamespaceObject*>(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);
}

View File

@@ -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.
});