fix: 修复 setupAxiosMock 多测试文件并发时 mock 丢失的问题

mock.module('axios', ...) 是 process-global last-write-wins,多个测试文件
各自注册时只有最后一个 handle 的闭包被保留,导致前面的测试 stub 不生效。
改为全局单例注册,所有 handle 共享一个 mock.module,路由器运行时扫描
活跃 handle 分派请求。

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-10 12:12:25 +08:00
parent 8fccd323a8
commit 80d4e095fd

View File

@@ -33,6 +33,7 @@
* objects with `isAxiosError: true`, set `axiosHandle.stubs.isAxiosError` — * objects with `isAxiosError: true`, set `axiosHandle.stubs.isAxiosError` —
* otherwise the real axios's predicate is used. * otherwise the real axios's predicate is used.
*/ */
import { mock } from 'bun:test' import { mock } from 'bun:test'
// Test stubs come in many shapes — `(url: string) => Promise<...>`, etc. — // Test stubs come in many shapes — `(url: string) => Promise<...>`, etc. —
@@ -63,14 +64,27 @@ export type AxiosMockHandle = {
stubs: AxiosMethodStubs stubs: AxiosMethodStubs
} }
// Global registry — all handles share one mock.module registration.
// The router scans handles in reverse order (most-recently activated first)
// to find one with `useStubs === true`.
let handles: AxiosMockHandle[] = []
let moduleRegistered = false
/** /**
* Register a process-global mock for `axios` that spreads the real module and * Register a process-global mock for `axios` that spreads the real module and
* gates each method behind a per-suite flag. Call once at the top of a test * gates each method behind a per-suite flag. Call once at the top of a test
* file (outside `describe`). Returns a handle whose `.useStubs` and `.stubs` * file (outside `describe`). Returns a handle whose `.useStubs` and `.stubs`
* fields the suite controls in beforeAll/afterAll. * fields the suite controls in beforeAll/afterAll.
*
* Multiple test files can call this safely — the `mock.module` is registered
* only once, and each handle is independent.
*/ */
export function setupAxiosMock(): AxiosMockHandle { export function setupAxiosMock(): AxiosMockHandle {
const handle: AxiosMockHandle = { useStubs: false, stubs: {} } const handle: AxiosMockHandle = { useStubs: false, stubs: {} }
handles.push(handle)
if (!moduleRegistered) {
moduleRegistered = true
mock.module('axios', () => { mock.module('axios', () => {
// Pull the REAL module synchronously inside the factory. Top-level // Pull the REAL module synchronously inside the factory. Top-level
@@ -84,9 +98,15 @@ export function setupAxiosMock(): AxiosMockHandle {
const route = (method: keyof AxiosMethodStubs): AnyFn => { const route = (method: keyof AxiosMethodStubs): AnyFn => {
const realFn = realDefault[method] as AnyFn | undefined const realFn = realDefault[method] as AnyFn | undefined
return (...args: unknown[]) => { return (...args: unknown[]) => {
if (handle.useStubs) { // Scan from the end so the most recently activated handle wins.
const stub = handle.stubs[method] as AnyFn | undefined for (let i = handles.length - 1; i >= 0; i--) {
const h = handles[i]
if (h.useStubs) {
const stub = h.stubs[method] as AnyFn | undefined
if (stub) return stub(...args) if (stub) return stub(...args)
// If the handle is active but has no stub for this method,
// fall through to the next active handle (or real axios).
}
} }
if (typeof realFn === 'function') return realFn(...args) if (typeof realFn === 'function') return realFn(...args)
throw new Error(`axios.${method} is not available on real axios`) throw new Error(`axios.${method} is not available on real axios`)
@@ -111,8 +131,11 @@ export function setupAxiosMock(): AxiosMockHandle {
} }
routedDefault.isAxiosError = (e: unknown) => { routedDefault.isAxiosError = (e: unknown) => {
if (handle.useStubs && handle.stubs.isAxiosError) { for (let i = handles.length - 1; i >= 0; i--) {
return handle.stubs.isAxiosError(e) const h = handles[i]
if (h.useStubs && h.stubs.isAxiosError) {
return h.stubs.isAxiosError(e)
}
} }
const realPredicate = realDefault.isAxiosError as const realPredicate = realDefault.isAxiosError as
| ((e: unknown) => boolean) | ((e: unknown) => boolean)
@@ -120,8 +143,11 @@ export function setupAxiosMock(): AxiosMockHandle {
return realPredicate ? realPredicate(e) : false return realPredicate ? realPredicate(e) : false
} }
routedDefault.isCancel = (e: unknown) => { routedDefault.isCancel = (e: unknown) => {
if (handle.useStubs && handle.stubs.isCancel) { for (let i = handles.length - 1; i >= 0; i--) {
return handle.stubs.isCancel(e) const h = handles[i]
if (h.useStubs && h.stubs.isCancel) {
return h.stubs.isCancel(e)
}
} }
const realPredicate = realDefault.isCancel as const realPredicate = realDefault.isCancel as
| ((e: unknown) => boolean) | ((e: unknown) => boolean)
@@ -135,6 +161,7 @@ export function setupAxiosMock(): AxiosMockHandle {
default: routedDefault, default: routedDefault,
} }
}) })
}
return handle return handle
} }