diff --git a/docs/test/mocks.mdx b/docs/test/mocks.mdx index 61f5b4ec0993..e6a74f854bef 100644 --- a/docs/test/mocks.mdx +++ b/docs/test/mocks.mdx @@ -341,6 +341,8 @@ test("mock.module", async () => { Like the rest of Bun, module mocks support both `import` and `require`. +`require()` can't wait for a promise. Before any `import` of the module, `require()` doesn't call an `async` factory that uses `await`. It returns the original module instead, as in Vitest. To get the mock from `require()`, import the module first. The `import` calls the factory, and `require()` returns the mock once the factory's promise is fulfilled. + ### Overriding Already Imported Modules Calling `mock.module()` overrides the module even if it has already been imported. diff --git a/test/js/bun/test/mock/mock-module.test.ts b/test/js/bun/test/mock/mock-module.test.ts index bbb5cefaa49f..369140a83d6f 100644 --- a/test/js/bun/test/mock/mock-module.test.ts +++ b/test/js/bun/test/mock/mock-module.test.ts @@ -897,6 +897,46 @@ describe.concurrent("the original of a mocked module", () => { }, ); + test.each(["mock.module", "vi.mock", "jest.mock"])( + "before any import of the module, require() returns the original and does not call an async factory of %s() that awaits", + async mockModule => { + await expectFixturesToPass( + { + ...modules, + "mod.cjs": `exports.value = "real";`, + "waits.test.ts": ` + import { expect, jest, mock, test, vi } from "bun:test"; + const state = vi.hoisted(() => ({ calls: [], rejects: 0 })); + ${mockModule}("./mod", async () => (state.calls.push("./mod"), await 1, { value: "mocked" })); + ${mockModule}("./mod.cjs", async () => (state.calls.push("./mod.cjs"), await 1, { value: "mocked" })); + ${mockModule}("cjs-pkg", async () => (state.calls.push("cjs-pkg"), await 1, { hello: () => "mocked" })); + ${mockModule}("node:os", async () => (state.calls.push("node:os"), await 1, { platform: () => "mocked" })); + ${mockModule}("./sub/thing", async () => { + await 1; + if (++state.rejects === 1) throw new Error("rejected"); + return { thing: "mocked" }; + }); + const required = () => [require("./mod").value, require("./mod.cjs").value, require("cjs-pkg").hello(), require("node:os").platform()]; + test("a file, a CommonJS file, a package and a builtin", async () => { + const originals = ["real", "real", "real-hello", process.platform]; + expect([required(), required(), state.calls]).toEqual([originals, originals, []]); + const imported = [await import("./mod"), await import("./mod.cjs"), await import("cjs-pkg"), await import("node:os")]; + expect([imported[0].value, imported[1].value, imported[2].hello(), imported[3].platform()]).toEqual(Array(4).fill("mocked")); + expect([required(), state.calls]).toEqual([Array(4).fill("mocked"), ["./mod", "./mod.cjs", "cjs-pkg", "node:os"]]); + }); + test("and after an import whose factory rejected, until one is fulfilled", async () => { + expect([require("./sub/thing").thing, state.rejects]).toEqual(["real-thing", 0]); + await expect(import("./sub/thing")).rejects.toThrow("rejected"); + expect([require("./sub/thing").thing, state.rejects]).toEqual(["real-thing", 1]); + expect([(await import("./sub/thing")).thing, require("./sub/thing").thing, state.rejects]).toEqual(["mocked", "mocked", 2]); + }); + `, + }, + 2, + ); + }, + ); + test("jest.requireActual", async () => { await expectFixturesToPass( {