From af50e009f13771cd4fe5f5b1eb1a280713df0f1c Mon Sep 17 00:00:00 2001 From: Kade Robertson Date: Mon, 8 Jun 2026 08:31:48 -0400 Subject: [PATCH 1/2] fix: stale mock metadata breaks automocking with isolate:false --- .../src/runtime/moduleRunner/moduleRunner.ts | 23 ++++--- test/e2e/test/mocking.test.ts | 64 ++++++++++++++++++- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts index be655b57f8af..0df3a209a218 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts @@ -168,23 +168,28 @@ export class VitestModuleRunner if (mod.meta && 'mockedModule' in mod.meta) { const mockedModule = mod.meta.mockedModule as MockedModule const mockId = this.mocker.getMockPath(mod.id) + const currentMock = this.mocker.getDependencyMock(mod.id) // bypass mock and force "importActual" behavior when: // - mock was removed by doUnmock (stale mockedModule in meta) + if (!currentMock) { + const node = await this.fetchModule(injectQuery(url, '_vitest_original')) + return this._cachedRequest(node.url, node, callstack, metadata) + } // - self-import: mock factory/file is importing the module it's mocking - const isStale = !this.mocker.getDependencyMock(mod.id) const isSelfImport = callstack.includes(mockId) || callstack.includes(url) - || ('redirect' in mockedModule && callstack.includes(mockedModule.redirect)) - if (isStale || isSelfImport) { + || ('redirect' in currentMock && callstack.includes(currentMock.redirect)) + if (isSelfImport) { const node = await this.fetchModule(injectQuery(url, '_vitest_original')) return this._cachedRequest(node.url, node, callstack, metadata) } - mocked = await this.mocker.requestWithMockedModule( - url, - mod, - callstack, - mockedModule, - ) + if (currentMock !== mockedModule) { + const freshNode = await this.fetchModule(injectQuery(url, '_vitest_original')) + mocked = await this.mocker.requestWithMockedModule(url, freshNode, callstack, currentMock) + } + else { + mocked = await this.mocker.requestWithMockedModule(url, mod, callstack, currentMock) + } } else { mocked = await this.mocker.mockedRequest(url, mod, callstack) diff --git a/test/e2e/test/mocking.test.ts b/test/e2e/test/mocking.test.ts index c3a14c8d2c5e..6234e543668a 100644 --- a/test/e2e/test/mocking.test.ts +++ b/test/e2e/test/mocking.test.ts @@ -5,7 +5,7 @@ import { playwright } from '@vitest/browser-playwright' import { webdriverio } from '@vitest/browser-webdriverio' import { afterAll, expect, test } from 'vitest' import { rolldownVersion } from 'vitest/node' -import { runInlineTests, runVitest } from '../../test-utils' +import { runInlineTests, runVitest, StableTestFileOrderSorter } from '../../test-utils' // webdriver@9 sets dns.setDefaultResultOrder("ipv4first") on import, // which makes Vite resolve localhost to 127.0.0.1 and breaks other tests asserting "localhost" @@ -594,3 +594,65 @@ test("local", async () => { } `) }) + +test('automocking works with isolate:false when factory mock runs first (resolve alias)', async () => { + const { stderr, testTree } = await runInlineTests({ + 'vitest.config.js': ` +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '~': path.resolve(import.meta.dirname, 'src'), + }, + }, + test: { + isolate: false, + }, +}) + `, + './src/dep.ts': ` +export function useDep(): string { return 'real' } +export function helperDep(): number { return 42 } + `, + './a-factory.test.ts': ` +import { vi, test, expect } from 'vitest' +import { useDep } from '~/dep' +vi.mock(import('~/dep'), () => ({ + useDep: () => 'factory', + helperDep: () => 0, +})) +test('factory mock', () => { + expect(useDep()).toBe('factory') +}) + `, + './b-automock.test.ts': ` +import { vi, test, expect } from 'vitest' +import { useDep } from '~/dep' +vi.mock(import('~/dep')) +test('automock exports are mock functions', () => { + expect(vi.isMockFunction(useDep)).toBe(true) +}) +test('automock mockReturnValue works', () => { + vi.mocked(useDep).mockReturnValue('mocked') + expect(useDep()).toBe('mocked') +}) + `, + }, { + sequence: { sequencer: StableTestFileOrderSorter }, + }) + + expect(stderr).toBe('') + expect(testTree()).toMatchInlineSnapshot(` + { + "a-factory.test.ts": { + "factory mock": "passed", + }, + "b-automock.test.ts": { + "automock exports are mock functions": "passed", + "automock mockReturnValue works": "passed", + }, + } + `) +}) From f394cb46d903fe450e0f88a0ec14edfa64645dc1 Mon Sep 17 00:00:00 2001 From: Kade Robertson Date: Tue, 9 Jun 2026 07:34:37 -0400 Subject: [PATCH 2/2] fix: avoid refetch on manual or redirect mocks --- packages/vitest/src/runtime/moduleRunner/moduleRunner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts index 0df3a209a218..c81b5ee89aee 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts @@ -183,7 +183,8 @@ export class VitestModuleRunner const node = await this.fetchModule(injectQuery(url, '_vitest_original')) return this._cachedRequest(node.url, node, callstack, metadata) } - if (currentMock !== mockedModule) { + const isAutoMock = currentMock.type === 'automock' || currentMock.type === 'autospy' + if (isAutoMock && currentMock !== mockedModule) { const freshNode = await this.fetchModule(injectQuery(url, '_vitest_original')) mocked = await this.mocker.requestWithMockedModule(url, freshNode, callstack, currentMock) }