diff --git a/examples/mocks/package.json b/examples/mocks/package.json index 706590929062..1bcb6741f1b3 100644 --- a/examples/mocks/package.json +++ b/examples/mocks/package.json @@ -19,7 +19,8 @@ "devDependencies": { "@vitest/ui": "latest", "vite": "^2.9.5", - "vitest": "latest" + "vitest": "latest", + "zustand": "4.0.0-rc.1" }, "stackblitz": { "startCommand": "npm run test:ui" diff --git a/examples/mocks/src/zustand.ts b/examples/mocks/src/zustand.ts new file mode 100644 index 000000000000..9d1c16beb43b --- /dev/null +++ b/examples/mocks/src/zustand.ts @@ -0,0 +1,14 @@ +import actualCreate from 'zustand' + +// a variable to hold reset functions for all stores declared in the app +const storeResetFns = new Set() + +// when creating a store, we get its initial state, create a reset function and add it in the set +const create = vi.fn((createState) => { + const store = actualCreate(createState) + const initialState = store.getState() + storeResetFns.add(() => store.setState(initialState, true)) + return store +}) + +export default create diff --git a/examples/mocks/test/factory.test.ts b/examples/mocks/test/factory.test.ts index a60a62a0fd29..c1a1ac73894a 100644 --- a/examples/mocks/test/factory.test.ts +++ b/examples/mocks/test/factory.test.ts @@ -1,6 +1,7 @@ import axios from 'axios' import * as example from '../src/example' import * as moduleA from '../src/moduleA' +import logger from '../src/log' vi .mock('../src/example', () => ({ @@ -28,6 +29,17 @@ vi.mock('axios', () => { } }) +vi.mock('../src/log.ts', async () => { + // can import the same module inside and does not go into an infinite loop + const log = await import('../src/log') + return { + default: { + ...log.default, + info: vi.fn(), + }, + } +}) + describe('mocking with factory', () => { test('successfuly mocked', () => { expect((example as any).mocked).toBe(true) @@ -44,4 +56,10 @@ describe('mocking with factory', () => { expect(axios.get).toHaveBeenCalledTimes(1) }) + + test('logger extended', () => { + expect(logger.warn).toBeTypeOf('function') + // @ts-expect-error extending module + expect(logger.info).toBeTypeOf('function') + }) }) diff --git a/examples/mocks/test/self-importing.test.ts b/examples/mocks/test/self-importing.test.ts new file mode 100644 index 000000000000..bb1d8f98bb5a --- /dev/null +++ b/examples/mocks/test/self-importing.test.ts @@ -0,0 +1,9 @@ +import zustand from 'zustand' + +vi.mock('zustand') + +describe('zustand didn\'t go into an infinite loop', () => { + test('zustand is mocked', () => { + expect(vi.isMockFunction(zustand)).toBe(true) + }) +}) diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index 1cd8823bbe42..ec2729ae0ed5 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -95,13 +95,6 @@ export class ViteNodeRunner { return `stack:\n${[...callstack, dep].reverse().map(p => `- ${p}`).join('\n')}` } - // probably means it was passed as variable - // and wasn't transformed by Vite - if (this.options.resolveId && this.shouldResolveId(dep)) { - const resolvedDep = await this.options.resolveId(dep, id) - dep = resolvedDep?.id?.replace(this.root, '') || dep - } - let debugTimer: any if (this.debug) debugTimer = setTimeout(() => this.debugLog(() => `module ${dep} takes over 2s to load.\n${getStack()}`), 2000) @@ -125,6 +118,27 @@ export class ViteNodeRunner { } } + Object.defineProperty(request, 'callstack', { get: () => callstack }) + + const resolveId = async (dep: string, callstackPosition = 1) => { + // probably means it was passed as variable + // and wasn't transformed by Vite + // or some dependency name was passed + // runner.executeFile('@scope/name') + // runner.executeFile(myDynamicName) + if (this.options.resolveId && this.shouldResolveId(dep)) { + let importer = callstack[callstack.length - callstackPosition] + if (importer && importer.startsWith('mock:')) + importer = importer.slice(5) + const { id } = await this.options.resolveId(dep, importer) || {} + dep = id && isAbsolute(id) ? `/@fs/${id}` : id || dep + } + + return dep + } + + id = await resolveId(id, 2) + const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS if (id in requestStubs) return requestStubs[id] @@ -157,6 +171,8 @@ export class ViteNodeRunner { }, } + let require: NodeRequire + // Be careful when changing this // changing context will change amount of code added on line :114 (vm.runInThisContext) // this messes up sourcemaps for coverage @@ -169,8 +185,15 @@ export class ViteNodeRunner { __vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj), __vite_ssr_import_meta__: { url }, + __vitest_resolve_id__: resolveId, + // cjs compact - require: createRequire(url), + require: (path: string) => { + if (!require) + require = createRequire(url) + + return require(path) + }, exports, module: moduleProxy, __filename, @@ -193,7 +216,7 @@ export class ViteNodeRunner { } shouldResolveId(dep: string) { - if (isNodeBuiltin(dep) || dep in (this.options.requestStubs || DEFAULT_REQUEST_STUBS)) + if (isNodeBuiltin(dep) || dep in (this.options.requestStubs || DEFAULT_REQUEST_STUBS) || dep.startsWith('/@vite')) return false return !isAbsolute(dep) || !extname(dep) diff --git a/packages/vitest/src/integrations/coverage.ts b/packages/vitest/src/integrations/coverage.ts index dbdcead66a43..e4c7243595c5 100644 --- a/packages/vitest/src/integrations/coverage.ts +++ b/packages/vitest/src/integrations/coverage.ts @@ -83,7 +83,7 @@ export async function reportCoverage(ctx: Vitest) { // This is a magic number. It corresponds to the amount of code // that we add in packages/vite-node/src/client.ts:114 (vm.runInThisContext) // TODO: Include our transformations in sourcemaps - const offset = 203 + const offset = 224 report._getSourceMap = (coverage: Profiler.ScriptCoverage) => { const path = pathToFileURL(coverage.url).href diff --git a/packages/vitest/src/integrations/spy.ts b/packages/vitest/src/integrations/spy.ts index b9f0401160a3..e69d67a9f79a 100644 --- a/packages/vitest/src/integrations/spy.ts +++ b/packages/vitest/src/integrations/spy.ts @@ -167,7 +167,7 @@ function enhanceSpy( }) }, get lastCall() { - return stub.calls.at(-1) + return stub.calls[stub.calls.length - 1] }, } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index d01b47c2db6a..a8d65530b3ae 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -78,12 +78,12 @@ export class Vitest { fetchModule(id: string) { return node.fetchModule(id) }, - resolveId(id: string, importer: string | undefined) { + resolveId(id: string, importer?: string) { return node.resolveId(id, importer) }, }) - this.reporters = await createReporters(resolved.reporters, this.runner.executeFile.bind(this.runner)) + this.reporters = await createReporters(resolved.reporters, this.runner) this.runningPromise = undefined diff --git a/packages/vitest/src/node/reporters/utils.ts b/packages/vitest/src/node/reporters/utils.ts index 6ffc0065a568..1380f2d12904 100644 --- a/packages/vitest/src/node/reporters/utils.ts +++ b/packages/vitest/src/node/reporters/utils.ts @@ -1,11 +1,12 @@ +import type { ViteNodeRunner } from 'vite-node/client' import type { Reporter } from '../../types' import { ReportersMap } from './index' import type { BuiltinReporters } from './index' -async function loadCustomReporterModule(path: string, fetchModule: (id: string) => Promise): Promise C> { +async function loadCustomReporterModule(path: string, runner: ViteNodeRunner): Promise C> { let customReporterModule: { default: new () => C } try { - customReporterModule = await fetchModule(path) + customReporterModule = await runner.executeId(path) } catch (customReporterModuleError) { throw new Error(`Failed to load custom Reporter from ${path}`, { cause: customReporterModuleError as Error }) @@ -17,7 +18,7 @@ async function loadCustomReporterModule(path: string, fetchM return customReporterModule.default } -function createReporters(reporterReferences: Array, fetchModule: (id: string) => Promise) { +function createReporters(reporterReferences: Array, runner: ViteNodeRunner) { const promisedReporters = reporterReferences.map(async (referenceOrInstance) => { if (typeof referenceOrInstance === 'string') { if (referenceOrInstance in ReportersMap) { @@ -25,7 +26,7 @@ function createReporters(reporterReferences: Array) { const request = context.__vite_ssr_import__ + const resolveId = context.__vitest_resolve_id__ const mocker = this.mocker.withRequest(request) @@ -49,8 +50,8 @@ export class VitestRunner extends ViteNodeRunner { } return Object.assign(context, { - __vite_ssr_import__: (dep: string) => mocker.requestWithMock(dep), - __vite_ssr_dynamic_import__: (dep: string) => mocker.requestWithMock(dep), + __vite_ssr_import__: async (dep: string) => mocker.requestWithMock(await resolveId(dep)), + __vite_ssr_dynamic_import__: async (dep: string) => mocker.requestWithMock(await resolveId(dep)), __vitest_mocker__: mocker, }) } diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 48993c1e8d75..9b5c9c912e29 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -10,11 +10,16 @@ import type { ExecuteOptions } from './execute' type Callback = (...args: any[]) => unknown +interface ViteRunnerRequest { + (dep: string): any + callstack: string[] +} + export class VitestMocker { private static pendingIds: PendingSuiteMock[] = [] private static spyModule?: typeof import('../integrations/spy') - private request!: (dep: string) => unknown + private request!: ViteRunnerRequest private root: string private callbacks: Record unknown)[]> = {} @@ -22,7 +27,7 @@ export class VitestMocker { constructor( public options: ExecuteOptions, private moduleCache: ModuleCacheMap, - request?: (dep: string) => unknown, + request?: ViteRunnerRequest, ) { this.root = this.options.root this.request = request! @@ -230,6 +235,8 @@ export class VitestMocker { const mock = this.getDependencyMock(dep) + const callstack = this.request.callstack + if (mock === null) { const cacheName = `${dep}__mock` const cache = this.moduleCache.get(cacheName) @@ -241,9 +248,11 @@ export class VitestMocker { this.emit('mocked', cacheName, { exports }) return exports } - if (typeof mock === 'function') + if (typeof mock === 'function' && !callstack.includes(`mock:${dep}`)) { + callstack.push(`mock:${dep}`) return this.callFunctionMock(dep, mock) - if (typeof mock === 'string') + } + if (typeof mock === 'string' && !callstack.includes(mock)) dep = mock return this.request(dep) } @@ -256,7 +265,7 @@ export class VitestMocker { VitestMocker.pendingIds.push({ type: 'unmock', id, importer }) } - public withRequest(request: (dep: string) => unknown) { + public withRequest(request: ViteRunnerRequest) { return new VitestMocker(this.options, this.moduleCache, request) } } diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index fe827f25c852..f566ed2f21f0 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -10,7 +10,6 @@ import { rpc } from './rpc' let _viteNode: { run: (files: string[], config: ResolvedConfig) => Promise - collect: (files: string[], config: ResolvedConfig) => Promise } const moduleCache = new ModuleCacheMap() @@ -37,7 +36,7 @@ async function startViteNode(ctx: WorkerContext) { const { config } = ctx - const { run, collect } = (await executeInViteNode({ + const { run } = (await executeInViteNode({ files: [ resolve(distDir, 'entry.js'), ], @@ -54,7 +53,7 @@ async function startViteNode(ctx: WorkerContext) { base: config.base, }))[0] - _viteNode = { run, collect } + _viteNode = { run } return _viteNode } @@ -86,17 +85,15 @@ function init(ctx: WorkerContext) { ), } - if (ctx.invalidates) - ctx.invalidates.forEach(i => moduleCache.delete(i)) + if (ctx.invalidates) { + ctx.invalidates.forEach((i) => { + moduleCache.delete(i) + moduleCache.delete(`${i}__mock`) + }) + } ctx.files.forEach(i => moduleCache.delete(i)) } -export async function collect(ctx: WorkerContext) { - init(ctx) - const { collect } = await startViteNode(ctx) - return collect(ctx.files, ctx.config) -} - export async function run(ctx: WorkerContext) { init(ctx) const { run } = await startViteNode(ctx) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df4000af42e4..804597e5f2d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,7 @@ importers: tinyspy: ^0.3.2 vite: ^2.9.5 vitest: workspace:* + zustand: 4.0.0-rc.1 dependencies: '@vueuse/integrations': 8.3.1_axios@0.26.1 axios: 0.26.1 @@ -172,6 +173,7 @@ importers: '@vitest/ui': link:../../packages/ui vite: 2.9.5 vitest: link:../../packages/vitest + zustand: 4.0.0-rc.1 examples/nextjs: specifiers: @@ -18661,6 +18663,12 @@ packages: use-isomorphic-layout-effect: 1.1.2_zdsfwtvwq54q3oqxwtq4jnbhh4 dev: true + /use-sync-external-store/1.1.0: + resolution: {integrity: sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dev: true + /use-sync-external-store/1.1.0_react@17.0.2: resolution: {integrity: sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==} peerDependencies: @@ -19647,6 +19655,21 @@ packages: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} dev: false + /zustand/4.0.0-rc.1: + resolution: {integrity: sha512-qgcs7zLqBdHu0PuT3GW4WCIY5SgXdsv30GQMu9Qpp1BA2aS+sNS8l4x0hWuyEhjXkN+701aGWawhKDv6oWJAcw==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + use-sync-external-store: 1.1.0 + dev: true + /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true diff --git a/test/reporters/tests/utils.test.ts b/test/reporters/tests/utils.test.ts index 2c9a25031df0..c39cecd04649 100644 --- a/test/reporters/tests/utils.test.ts +++ b/test/reporters/tests/utils.test.ts @@ -2,13 +2,16 @@ * @format */ import { resolve } from 'pathe' +import type { ViteNodeRunner } from 'vite-node/client' import { describe, expect, test } from 'vitest' import { createReporters } from 'vitest/src/node/reporters/utils' import { DefaultReporter } from '../../../../vitest/packages/vitest/src/node/reporters/default' import TestReporter from '../src/custom-reporter' const customReporterPath = resolve(__dirname, '../src/custom-reporter.js') -const fetchModule = (id: string) => import(id) +const fetchModule = { + executeId: (id: string) => import(id), +} as ViteNodeRunner describe('Reporter Utils', () => { test('passing an empty array returns nothing', async () => {