diff --git a/examples/mocks/test/automocking.spec.ts b/examples/mocks/test/automocking.spec.ts index eb9dba8979b1..b99029e0e3d3 100644 --- a/examples/mocks/test/automocking.spec.ts +++ b/examples/mocks/test/automocking.spec.ts @@ -1,8 +1,10 @@ import type * as exampleModule from '../src/example' import log from '../src/log' +import { A } from '../src/moduleA' import { methodSymbol, moduleWithSymbol } from '../src/moduleWithSymbol' vi.mock('../src/log') +vi.mock('../src/moduleA') vi.mock('../src/moduleWithSymbol') test('all mocked are valid', async () => { @@ -55,3 +57,7 @@ test('automock properly restores mock', async () => { expect(moduleWithSymbol[methodSymbol]()).toBe('hello') expect(moduleWithSymbol.warn()).toBe('hello') }) + +test('automock has a getter', () => { + expect(A).toBe('A') +}) diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 1fe74cb6482c..acecf9eeffa0 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -209,10 +209,14 @@ export class VitestMocker { const isModule = containerType === 'Module' || !!container.__esModule for (const { key: property, descriptor } of getAllMockableProperties(container)) { // Modules define their exports as getters. We want to process those. - if (!isModule) { - // TODO: Mock getters/setters somehow? - if (descriptor.get || descriptor.set) - continue + if (!isModule && descriptor.get) { + try { + Object.defineProperty(newContainer, property, descriptor) + } + catch (error) { + // Ignore errors, just move on to the next prop. + } + continue } // Skip special read-only props, we don't want to mess with those. diff --git a/test/core/src/mockedC.ts b/test/core/src/mockedC.ts index cead9be3bcc8..84477be0862f 100644 --- a/test/core/src/mockedC.ts +++ b/test/core/src/mockedC.ts @@ -11,6 +11,10 @@ export class MockedC { return mockedA() } + get getOnlyProp(): number { + return 42 + } + get getSetProp(): number { return 123 } diff --git a/test/core/test/mocked.test.js b/test/core/test/mocked.test.js index 58735f29f545..b96d9691432c 100644 --- a/test/core/test/mocked.test.js +++ b/test/core/test/mocked.test.js @@ -1,4 +1,4 @@ -// this file should not be converted to js +// this file should not be converted to ts // so it won't be transformed by esbuild import { assert, test, vi } from 'vitest' diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts index 13be6b932688..a329cdf08ff8 100644 --- a/test/core/test/mocked.test.ts +++ b/test/core/test/mocked.test.ts @@ -13,6 +13,25 @@ vitest.mock('virtual-module', () => ({ value: 'mock' })) vitest.mock('../src/mockedC') vitest.mock('../src/mockedD') +/** + * Get a property descriptor from an object. + * + * This is different from `Object.getOwnPropertyDescriptor` because it recurses + * into the prototype chain until it either finds a match or reaches the end. + * + * @param object The object that contains the property. + * @param property The property. + * @returns The property's descriptor, or undefined if no matching property was found. + */ +function getPropertyDescriptor(object: any, property: PropertyKey) { + for (let o = object; o; o = Object.getPrototypeOf(o)) { + const descriptor = Object.getOwnPropertyDescriptor(o, property) + if (descriptor) + return descriptor + } + return undefined +} + test('submodule is mocked to return "two" as 3', () => { assert.equal(3, two) }) @@ -58,6 +77,43 @@ describe('mocked classes', () => { expect(MockedC.prototype.doSomething).toHaveBeenCalledOnce() expect(MockedC.prototype.doSomething).not.toHaveReturnedWith('A') }) + + test('should mock getters', () => { + const instance = new MockedC() + + expect(instance).toHaveProperty('getOnlyProp') + const descriptor = getPropertyDescriptor(instance, 'getOnlyProp') + expect(descriptor?.get).toBeDefined() + expect(descriptor?.set).not.toBeDefined() + + expect(instance.getOnlyProp).toBe(42) + // @ts-expect-error Assign to the read-only prop to ensure it errors. + expect(() => instance.getOnlyProp = 4).toThrow() + + const getterSpy = vi.spyOn(instance, 'getOnlyProp', 'get').mockReturnValue(456) + expect(instance.getOnlyProp).toEqual(456) + expect(getterSpy).toHaveBeenCalledOnce() + }) + + test('should mock getters and setters', () => { + const instance = new MockedC() + + expect(instance).toHaveProperty('getSetProp') + const descriptor = getPropertyDescriptor(instance, 'getSetProp') + expect(descriptor?.get).toBeDefined() + expect(descriptor?.set).toBeDefined() + + expect(instance.getSetProp).toBe(123) + expect(() => instance.getSetProp = 4).not.toThrow() + + const getterSpy = vi.spyOn(instance, 'getSetProp', 'get').mockReturnValue(789) + expect(instance.getSetProp).toEqual(789) + expect(getterSpy).toHaveBeenCalledOnce() + + const setterSpy = vi.spyOn(instance, 'getSetProp', 'set') + instance.getSetProp = 159 + expect(setterSpy).toHaveBeenCalledWith(159) + }) }) describe('default exported classes', () => {