diff --git a/examples/mocks/src/default.ts b/examples/mocks/src/default.ts new file mode 100644 index 000000000..79ddac90e --- /dev/null +++ b/examples/mocks/src/default.ts @@ -0,0 +1 @@ +export default 'a default' diff --git a/examples/mocks/src/example.ts b/examples/mocks/src/example.ts index 22f15d20f..eef15ace3 100644 --- a/examples/mocks/src/example.ts +++ b/examples/mocks/src/example.ts @@ -25,3 +25,4 @@ export const number = 123 export const string = 'baz' export const boolean = true export const symbol = Symbol.for('a.b.c') +export default 'a default' diff --git a/examples/mocks/test/factory.test.ts b/examples/mocks/test/factory.test.ts index c1a1ac738..0cf1822eb 100644 --- a/examples/mocks/test/factory.test.ts +++ b/examples/mocks/test/factory.test.ts @@ -6,6 +6,9 @@ import logger from '../src/log' vi .mock('../src/example', () => ({ mocked: true, + then: 'a then export', + square: (a: any, b: any) => a + b, + asyncSquare: async (a: any, b: any) => Promise.resolve(a + b), })) // doesn't think comments are mocks @@ -40,10 +43,27 @@ vi.mock('../src/log.ts', async () => { } }) +vi.mock('../src/default.ts', () => null) + describe('mocking with factory', () => { - test('successfuly mocked', () => { + test('missing exports on mock', () => { + expect(() => example.default).toThrowError('[vitest] No "default" export is defined on the "mock:/src/example.ts"') + expect(() => example.boolean).toThrowError('[vitest] No "boolean" export is defined on the "mock:/src/example.ts"') + expect(() => example.object).toThrowError('[vitest] No "object" export is defined on the "mock:/src/example.ts"') + expect(() => example.array).toThrowError('[vitest] No "array" export is defined on the "mock:/src/example.ts"') + expect(() => example.someClasses).toThrowError('[vitest] No "someClasses" export is defined on the "mock:/src/example.ts"') + }) + + it('non-object return on factory gives error', async () => { + await expect(() => import('../src/default').then(m => m.default)).rejects + .toThrowError('[vitest] vi.mock(path: string, factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?') + }) + + test('defined exports on mock', async () => { + expect((example as any).then).toBe('a then export') expect((example as any).mocked).toBe(true) - expect(example.boolean).toBeUndefined() + expect(example.square(2, 3)).toBe(5) + expect(example.asyncSquare(2, 3)).resolves.toBe(5) }) test('successfuly with actual', () => { diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index de63b8734..7361cbff8 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -107,8 +107,30 @@ export class VitestMocker { if (cached) return cached const exports = await mock() + + if (exports === null || typeof exports !== 'object') + throw new Error('[vitest] vi.mock(path: string, factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?') + this.moduleCache.set(dep, { exports }) - return exports + + const exportHandler = { + get(target: Record, prop: any) { + const val = target[prop] + + // 'then' can exist on non-Promise objects, need nested instanceof check for logic to work + if (prop === 'then') { + if (target instanceof Promise) + return target.then.bind(target) + } + else if (val === undefined) { + throw new Error(`[vitest] No "${prop}" export is defined on the "${dep}"`) + } + + return val + }, + } + + return new Proxy(exports, exportHandler) } private getMockPath(dep: string) {