mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
fix(jest-mock): remove leftover own accessor when restoring spyOn of an inherited getter/setter (#16226)
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user