From 3fa3d527a013ed3c6dc7df41096903ce6d466c01 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 18 Mar 2026 14:10:43 +0900 Subject: [PATCH 1/2] fix: re-evaluate actual modules of mocked external --- .../vitest/src/runtime/moduleRunner/startVitestModuleRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/runtime/moduleRunner/startVitestModuleRunner.ts b/packages/vitest/src/runtime/moduleRunner/startVitestModuleRunner.ts index 33ce48443729..22d7eb6c9988 100644 --- a/packages/vitest/src/runtime/moduleRunner/startVitestModuleRunner.ts +++ b/packages/vitest/src/runtime/moduleRunner/startVitestModuleRunner.ts @@ -135,7 +135,7 @@ export function startVitestModuleRunner(options: ContextModuleRunnerOptions): Vi // if module is invalidated, the worker will be recreated, // so cached is always true in a single worker - if (options?.cached) { + if (!isImportActual && options?.cached) { return { cache: true } } From 0f45c272c3d710f9045a49a3ca14b4ff7316fe9f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 18 Mar 2026 14:51:49 +0900 Subject: [PATCH 2/2] test: add test --- pnpm-lock.yaml | 8 ++ test/cli/deps/dep-simple/index.js | 1 + test/cli/deps/dep-simple/package.json | 6 ++ test/cli/package.json | 1 + test/cli/test/mocking.test.ts | 149 ++++++++++++++++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 test/cli/deps/dep-simple/index.js create mode 100644 test/cli/deps/dep-simple/package.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index badb1d146786..a907a964e130 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1311,6 +1311,9 @@ importers: test-dep-invalid: specifier: link:./deps/dep-invalid version: link:deps/dep-invalid + test-dep-simple: + specifier: file:./deps/dep-simple + version: file:test/cli/deps/dep-simple tinyspy: specifier: 'catalog:' version: 4.0.4 @@ -9782,6 +9785,9 @@ packages: test-dep-error@file:test/browser/deps/test-dep-error: resolution: {directory: test/browser/deps/test-dep-error, type: directory} + test-dep-simple@file:test/cli/deps/dep-simple: + resolution: {directory: test/cli/deps/dep-simple, type: directory} + text-decoder@1.1.1: resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} @@ -19163,6 +19169,8 @@ snapshots: test-dep-error@file:test/browser/deps/test-dep-error: {} + test-dep-simple@file:test/cli/deps/dep-simple: {} + text-decoder@1.1.1: dependencies: b4a: 1.6.4 diff --git a/test/cli/deps/dep-simple/index.js b/test/cli/deps/dep-simple/index.js new file mode 100644 index 000000000000..0d74b5ea77ed --- /dev/null +++ b/test/cli/deps/dep-simple/index.js @@ -0,0 +1 @@ +export default 'test-dep-simple' diff --git a/test/cli/deps/dep-simple/package.json b/test/cli/deps/dep-simple/package.json new file mode 100644 index 000000000000..bb33afe54b28 --- /dev/null +++ b/test/cli/deps/dep-simple/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-dep-simple", + "type": "module", + "private": true, + "exports": "./index.js" +} diff --git a/test/cli/package.json b/test/cli/package.json index 41977b9ff895..714a061ef545 100644 --- a/test/cli/package.json +++ b/test/cli/package.json @@ -30,6 +30,7 @@ "obug": "^2.1.1", "playwright": "catalog:", "test-dep-invalid": "link:./deps/dep-invalid", + "test-dep-simple": "file:./deps/dep-simple", "tinyspy": "catalog:", "typescript": "catalog:", "unplugin-swc": "^1.5.9", diff --git a/test/cli/test/mocking.test.ts b/test/cli/test/mocking.test.ts index 0603417436c2..65dea4c0720b 100644 --- a/test/cli/test/mocking.test.ts +++ b/test/cli/test/mocking.test.ts @@ -391,3 +391,152 @@ test('mock works without loading original', () => { } `) }) + +test.for([ + 'node', + 'playwright', + 'webdriverio', +])('repeating mock, importActual, and resetModules (%s)', async (mode) => { + const { stderr, errorTree } = await runInlineTests({ + // external + './external.test.ts': ` +import { expect, test, vi } from "vitest" + +test("external", async () => { + vi.doMock(import("test-dep-simple"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib1: any = await import("test-dep-simple") + expect(lib1.default).toBe("test-dep-simple") + + vi.resetModules(); + vi.doMock(import("test-dep-simple"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib2: any = await import("test-dep-simple") + expect(lib2.default).toBe("test-dep-simple") + expect.soft(lib1 !== lib2).toBe(true) + + vi.resetModules(); + vi.doMock(import("test-dep-simple"), async () => ({ mocked: true })); + const lib3 = await import("test-dep-simple"); + expect(lib3).toMatchObject({ mocked: true }) + + const lib4 = await vi.importActual("test-dep-simple"); + expect(lib4.default).toBe("test-dep-simple") + const lib5 = await vi.importActual("test-dep-simple"); + expect(lib4).toBe(lib5) +}); + `, + // builtin module + './builtin.test.ts': ` +import { expect, test, vi } from "vitest" + +test("builtin", async () => { + vi.doMock(import("node:path"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib1: any = await import("node:path") + expect(lib1).toHaveProperty('join') + + vi.resetModules(); + vi.doMock(import("node:path"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib2: any = await import("node:path") + expect(lib2).toHaveProperty('join') + expect.soft(lib1 !== lib2).toBe(true) + + vi.resetModules(); + vi.doMock(import("node:path"), async () => ({ mocked: true })); + const lib3 = await import("node:path"); + expect(lib3).toMatchObject({ mocked: true }) + + const lib4 = await vi.importActual("node:path"); + expect(lib4).toHaveProperty('join') + const lib5 = await vi.importActual("node:path"); + expect(lib4).toBe(lib5) +}); + `, + // local module + './local.test.ts': ` +import { expect, test, vi } from "vitest" + +test("local", async () => { + vi.doMock(import("./local.js"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib1: any = await import("./local.js") + expect(lib1).toHaveProperty('local') + + vi.resetModules(); + vi.doMock(import("./local.js"), async (importActual) => { + const lib = await importActual(); + return lib; + }) + const lib2: any = await import("./local.js") + expect(lib2).toHaveProperty('local') + expect.soft(lib1 !== lib2).toBe(true) + + vi.resetModules(); + vi.doMock(import("./local.js"), async () => ({ mocked: true })); + const lib3 = await import("./local.js"); + expect(lib3).toMatchObject({ mocked: true }) + + const lib4 = await vi.importActual("./local.js"); + expect(lib4).toHaveProperty('local') + const lib5 = await vi.importActual("./local.js"); + expect(lib4).toBe(lib5) +}); + `, + './local.js': `export const local = 'local'`, + }, modeToConfig(mode)) + + if (mode === 'webdriverio' || mode === 'playwright') { + // browser mode doesn't support resetModules nor node builtin + expect(errorTree()).toMatchInlineSnapshot(` + { + "builtin.test.ts": { + "builtin": [ + "Cannot convert a Symbol value to a string", + ], + }, + "external.test.ts": { + "external": [ + "expected false to be true // Object.is equality", + "expected { default: 'test-dep-simple', …(1) } to match object { mocked: true } + (1 matching property omitted from actual)", + ], + }, + "local.test.ts": { + "local": [ + "expected false to be true // Object.is equality", + "expected { local: 'local', …(1) } to match object { mocked: true } + (1 matching property omitted from actual)", + ], + }, + } + `) + return + } + + expect(stderr).toMatchInlineSnapshot(`""`) + expect(errorTree()).toMatchInlineSnapshot(` + { + "builtin.test.ts": { + "builtin": "passed", + }, + "external.test.ts": { + "external": "passed", + }, + "local.test.ts": { + "local": "passed", + }, + } + `) +})