fix(jest-mock): remove leftover own accessor when restoring spyOn of an inherited getter/setter (#16226)

This commit is contained in:
Shaurya Singh
2026-06-21 14:25:13 +02:00
committed by GitHub
parent a5cab18535
commit f51db281ed
3 changed files with 99 additions and 5 deletions
+1
View File
@@ -12,6 +12,7 @@
- `[expect]` Widen `toMatchObject` and `objectContaining` parameter type from `Record<string, unknown>` to `object` so class instances are accepted ([#16196](https://github.com/jestjs/jest/pull/16196))
- `[jest-mock]` `mockResolvedValue` / `mockRejectedValue` now see all overload return types, so a Promise-returning overload survives even when a later overload returns a non-Promise (e.g. `pg.Client['end']`) ([#16237](https://github.com/jestjs/jest/pull/16237))
- `[@jest-environment/jsdom-abstract]` Make `@types/jsdom` a peer dependency ([#16166](https://github.com/jestjs/jest/pull/16166))
- `[jest-mock]` Remove the leftover own accessor descriptor when restoring a `spyOn` of an inherited getter or setter, so the instance keeps reflecting the prototype ([#16226](https://github.com/jestjs/jest/pull/16226))
- `[jest-runtime]` Fall back to native ESM when a `.js` file contains ESM syntax but has no `"type":"module"` marker ([#16152](https://github.com/jestjs/jest/pull/16152))
- `[jest-runtime]` Support older test environments whose `moduleMocker` does not implement `clearMocksOnScope` ([#16169](https://github.com/jestjs/jest/pull/16169))
@@ -1768,6 +1768,28 @@ describe('moduleMocker', () => {
expect(spy).not.toHaveBeenCalled();
});
it('does not leave an own descriptor when restoring a spy on an inherited getter-defined method', () => {
let calls = 0;
const parent = {
get method() {
return function () {
calls++;
};
},
};
const child = Object.create(parent);
const spy = moduleMocker.spyOn(child, 'method');
child.method();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
expect(Object.prototype.hasOwnProperty.call(child, 'method')).toBe(false);
child.method();
expect(calls).toBe(2);
});
it('should work with object of null prototype', () => {
const Foo = Object.assign(Object.create(null), {
foo() {},
@@ -1845,6 +1867,62 @@ describe('moduleMocker', () => {
expect(obj.property).toBe(true);
});
it('does not leave an own descriptor on the instance after restoring an inherited getter', () => {
const parent = {
get value() {
return 'parent';
},
};
const child = Object.create(parent);
const spy = moduleMocker
.spyOn(child, 'value', 'get')
.mockReturnValue('spied');
expect(child.value).toBe('spied');
spy.mockRestore();
expect(child.value).toBe('parent');
// The accessor was inherited, so restoring should not leave an own
// property shadowing the prototype.
expect(Object.prototype.hasOwnProperty.call(child, 'value')).toBe(false);
// Because the own property was removed, later changes to the prototype
// accessor are still observed through the instance.
Object.defineProperty(parent, 'value', {
configurable: true,
get() {
return 'parent updated';
},
});
expect(child.value).toBe('parent updated');
});
it('does not leave an own descriptor on the instance after restoring an inherited setter', () => {
const received: Array<unknown> = [];
let stored: unknown;
const parent = {
get value() {
return stored;
},
set value(next: unknown) {
stored = next;
received.push(next);
},
};
const child = Object.create(parent);
const spy = moduleMocker.spyOn(child, 'value', 'set');
child.value = 'a';
expect(spy).toHaveBeenCalledWith('a');
spy.mockRestore();
expect(Object.prototype.hasOwnProperty.call(child, 'value')).toBe(false);
child.value = 'b';
expect(received).toEqual(['a', 'b']);
});
it('should throw on invalid input', () => {
expect(() => {
moduleMocker.spyOn(null, 'method');
+20 -5
View File
@@ -1335,8 +1335,12 @@ export class ModuleMocker {
if (descriptor && descriptor.get) {
const originalGet = descriptor.get;
mock = this._makeComponent({type: 'function'}, () => {
descriptor!.get = originalGet;
Object.defineProperty(object, methodKey, descriptor!);
if (isMethodOwner) {
descriptor!.get = originalGet;
Object.defineProperty(object, methodKey, descriptor!);
} else {
delete object[methodKey];
}
});
descriptor.get = () => mock;
Object.defineProperty(object, methodKey, descriptor);
@@ -1365,6 +1369,11 @@ export class ModuleMocker {
propertyKey: keyof T,
accessType: 'get' | 'set',
): MockInstance {
const isPropertyOwner = Object.prototype.hasOwnProperty.call(
object,
propertyKey,
);
let descriptor = Object.getOwnPropertyDescriptor(object, propertyKey);
let proto = Object.getPrototypeOf(object);
@@ -1415,9 +1424,15 @@ export class ModuleMocker {
}
descriptor[accessType] = this._makeComponent({type: 'function'}, () => {
// @ts-expect-error: mock is assignable
descriptor![accessType] = original;
Object.defineProperty(object, propertyKey, descriptor!);
if (isPropertyOwner) {
// @ts-expect-error: mock is assignable
descriptor![accessType] = original;
Object.defineProperty(object, propertyKey, descriptor!);
} else {
// Inherited accessor: drop the own property the spy installed so the
// prototype accessor is exposed again instead of a stale copy.
delete object[propertyKey];
}
});
(descriptor[accessType] as Mock).mockImplementation(function (