From 80d4e095fd361ca23e468c5b691bd46dfef0b3f7 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 10 May 2026 12:12:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20setupAxiosMock=20?= =?UTF-8?q?=E5=A4=9A=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E6=97=B6=20mock=20=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mock.module('axios', ...) 是 process-global last-write-wins,多个测试文件 各自注册时只有最后一个 handle 的闭包被保留,导致前面的测试 stub 不生效。 改为全局单例注册,所有 handle 共享一个 mock.module,路由器运行时扫描 活跃 handle 分派请求。 Co-Authored-By: glm-5-turbo --- tests/mocks/axios.ts | 139 ++++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 56 deletions(-) diff --git a/tests/mocks/axios.ts b/tests/mocks/axios.ts index 508065017..7f2a74a5d 100644 --- a/tests/mocks/axios.ts +++ b/tests/mocks/axios.ts @@ -33,6 +33,7 @@ * objects with `isAxiosError: true`, set `axiosHandle.stubs.isAxiosError` — * otherwise the real axios's predicate is used. */ + import { mock } from 'bun:test' // Test stubs come in many shapes — `(url: string) => Promise<...>`, etc. — @@ -63,78 +64,104 @@ export type AxiosMockHandle = { 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 * 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` * 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 { const handle: AxiosMockHandle = { useStubs: false, stubs: {} } + handles.push(handle) - mock.module('axios', () => { - // Pull the REAL module synchronously inside the factory. Top-level - // `await import('axios')` would resolve through the mock and recurse. - // eslint-disable-next-line @typescript-eslint/no-require-imports - const real = require('axios') as Record - const realDefault = ((real.default as - | Record - | undefined) ?? real) as Record + if (!moduleRegistered) { + moduleRegistered = true - const route = (method: keyof AxiosMethodStubs): AnyFn => { - const realFn = realDefault[method] as AnyFn | undefined - return (...args: unknown[]) => { - if (handle.useStubs) { - const stub = handle.stubs[method] as AnyFn | undefined - if (stub) return stub(...args) + mock.module('axios', () => { + // Pull the REAL module synchronously inside the factory. Top-level + // `await import('axios')` would resolve through the mock and recurse. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const real = require('axios') as Record + const realDefault = ((real.default as + | Record + | undefined) ?? real) as Record + + const route = (method: keyof AxiosMethodStubs): AnyFn => { + const realFn = realDefault[method] as AnyFn | undefined + return (...args: unknown[]) => { + // Scan from the end so the most recently activated handle wins. + 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 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) + throw new Error(`axios.${method} is not available on real axios`) } - if (typeof realFn === 'function') return realFn(...args) - throw new Error(`axios.${method} is not available on real axios`) } - } - const verbs: (keyof AxiosMethodStubs)[] = [ - 'get', - 'post', - 'put', - 'patch', - 'delete', - 'head', - 'options', - 'request', - 'create', - ] + const verbs: (keyof AxiosMethodStubs)[] = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'head', + 'options', + 'request', + 'create', + ] - const routedDefault: Record = { ...realDefault } - for (const v of verbs) { - routedDefault[v] = route(v) - } - - routedDefault.isAxiosError = (e: unknown) => { - if (handle.useStubs && handle.stubs.isAxiosError) { - return handle.stubs.isAxiosError(e) + const routedDefault: Record = { ...realDefault } + for (const v of verbs) { + routedDefault[v] = route(v) } - const realPredicate = realDefault.isAxiosError as - | ((e: unknown) => boolean) - | undefined - return realPredicate ? realPredicate(e) : false - } - routedDefault.isCancel = (e: unknown) => { - if (handle.useStubs && handle.stubs.isCancel) { - return handle.stubs.isCancel(e) - } - const realPredicate = realDefault.isCancel as - | ((e: unknown) => boolean) - | undefined - return realPredicate ? realPredicate(e) : false - } - return { - ...real, - ...routedDefault, - default: routedDefault, - } - }) + routedDefault.isAxiosError = (e: unknown) => { + for (let i = handles.length - 1; i >= 0; i--) { + const h = handles[i] + if (h.useStubs && h.stubs.isAxiosError) { + return h.stubs.isAxiosError(e) + } + } + const realPredicate = realDefault.isAxiosError as + | ((e: unknown) => boolean) + | undefined + return realPredicate ? realPredicate(e) : false + } + routedDefault.isCancel = (e: unknown) => { + for (let i = handles.length - 1; i >= 0; i--) { + const h = handles[i] + if (h.useStubs && h.stubs.isCancel) { + return h.stubs.isCancel(e) + } + } + const realPredicate = realDefault.isCancel as + | ((e: unknown) => boolean) + | undefined + return realPredicate ? realPredicate(e) : false + } + + return { + ...real, + ...routedDefault, + default: routedDefault, + } + }) + } return handle }