From 3c3ae3fb8af8f1cf4b18f2178ce6ca022a279229 Mon Sep 17 00:00:00 2001 From: naveentehrpariya Date: Fri, 12 Jun 2026 13:10:53 +0530 Subject: [PATCH] fix(spy): keep prototype methods on instances created from mocked classes --- packages/spy/src/index.ts | 18 ++++++ test/unit/test/jest-mock.test.ts | 102 ++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 125724845c71..0f52a8ceaf9e 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -499,6 +499,24 @@ function createMock( try { if (new.target) { + // The created instance inherits from `new.target.prototype`, which + // is the mock's own prototype when the mock is constructed directly, + // so prototype methods of the implementation would be lost. Link the + // implementation's prototype into the chain instead; methods + // assigned directly on `mock.prototype` keep priority. + const implementationPrototype = (implementation as Constructable).prototype + if ( + implementationPrototype + && typeof implementationPrototype === 'object' + // eslint-disable-next-line ts/no-use-before-define + && implementationPrototype !== mock.prototype + // eslint-disable-next-line ts/no-use-before-define + && Object.getPrototypeOf(mock.prototype) !== implementationPrototype + ) { + // eslint-disable-next-line ts/no-use-before-define + Object.setPrototypeOf(mock.prototype, implementationPrototype) + } + returnValue = Reflect.construct(implementation, args, new.target) // jest calls this before the implementation, but we have to resolve this _after_ diff --git a/test/unit/test/jest-mock.test.ts b/test/unit/test/jest-mock.test.ts index 566ae41ae2fb..035c0d1fe706 100644 --- a/test/unit/test/jest-mock.test.ts +++ b/test/unit/test/jest-mock.test.ts @@ -617,12 +617,112 @@ describe('jest mock compat layer', () => { Spy.mockImplementation(MockExample) expect(new Spy()).toBeInstanceOf(Spy) - expect(new Spy()).not.toBeInstanceOf(MockExample) + // the implementation's prototype is linked into the chain so that + // instances keep its prototype methods (#10553) + expect(new Spy()).toBeInstanceOf(MockExample) + expect(typeof new Spy().test).toBe('function') const instance = new Spy() expectTypeOf(instance).toEqualTypeOf() }) + it('instances created from a spied class keep prototype methods', () => { + class Example { + test() { + return 'original' + } + } + + const obj = { Example } + const Spy = vi.spyOn(obj, 'Example') + + const instance = new obj.Example() + + expect(instance).toBeInstanceOf(Spy) + expect(instance.test()).toBe('original') + expect(Spy).toHaveBeenCalledTimes(1) + }) + + it('mockImplementation with a class keeps prototype methods (#10553)', () => { + class OriginalClass { + method() { + return 'original' + } + } + + const myObj = { TestClass: OriginalClass } + + vi.spyOn(myObj, 'TestClass').mockImplementation( + class MockClass extends OriginalClass { + method() { + return 'mocked' + } + }, + ) + + const instance = new myObj.TestClass() + + expect(typeof instance.method).toBe('function') + expect(instance.method()).toBe('mocked') + expect(instance).toBeInstanceOf(myObj.TestClass) + expect(instance).toBeInstanceOf(OriginalClass) + }) + + it('vi.fn with a class keeps prototype methods', () => { + const MockedClass = vi.fn(class { + method() { + return 'fn-mocked' + } + }) + + const instance = new MockedClass() + + expect(instance.method()).toBe('fn-mocked') + expect(instance).toBeInstanceOf(MockedClass) + }) + + it('methods assigned on the mock prototype take priority over the implementation', () => { + class Example { + test() { + return 'class method' + } + } + + const obj = { Example } + const Spy = vi.spyOn(obj, 'Example') + ;(Spy.prototype as any).test = vi.fn(() => 'assigned method') + + const instance = new obj.Example() as any + + expect(instance.test()).toBe('assigned method') + }) + + it('mockImplementationOnce with different classes resolves each prototype', () => { + class Example { + which() { + return 'original' + } + } + + const obj = { Example } + const Spy = vi.spyOn(obj, 'Example') + + Spy.mockImplementationOnce(class { + which() { + return 'A' + } + } as typeof Example) + Spy.mockImplementationOnce(class { + which() { + return 'B' + } + } as typeof Example) + + expect(new obj.Example().which()).toBe('A') + expect(new obj.Example().which()).toBe('B') + expect(new obj.Example().which()).toBe('original') + }) + it('returns temporary implementations from getMockImplementation()', () => { const fn = vi.fn()