mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
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:
@@ -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,78 +64,104 @@ 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)
|
||||||
|
|
||||||
mock.module('axios', () => {
|
if (!moduleRegistered) {
|
||||||
// Pull the REAL module synchronously inside the factory. Top-level
|
moduleRegistered = true
|
||||||
// `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<string, unknown>
|
|
||||||
const realDefault = ((real.default as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined) ?? real) as Record<string, unknown>
|
|
||||||
|
|
||||||
const route = (method: keyof AxiosMethodStubs): AnyFn => {
|
mock.module('axios', () => {
|
||||||
const realFn = realDefault[method] as AnyFn | undefined
|
// Pull the REAL module synchronously inside the factory. Top-level
|
||||||
return (...args: unknown[]) => {
|
// `await import('axios')` would resolve through the mock and recurse.
|
||||||
if (handle.useStubs) {
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const stub = handle.stubs[method] as AnyFn | undefined
|
const real = require('axios') as Record<string, unknown>
|
||||||
if (stub) return stub(...args)
|
const realDefault = ((real.default as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined) ?? real) as Record<string, unknown>
|
||||||
|
|
||||||
|
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)[] = [
|
const verbs: (keyof AxiosMethodStubs)[] = [
|
||||||
'get',
|
'get',
|
||||||
'post',
|
'post',
|
||||||
'put',
|
'put',
|
||||||
'patch',
|
'patch',
|
||||||
'delete',
|
'delete',
|
||||||
'head',
|
'head',
|
||||||
'options',
|
'options',
|
||||||
'request',
|
'request',
|
||||||
'create',
|
'create',
|
||||||
]
|
]
|
||||||
|
|
||||||
const routedDefault: Record<string, unknown> = { ...realDefault }
|
const routedDefault: Record<string, unknown> = { ...realDefault }
|
||||||
for (const v of verbs) {
|
for (const v of verbs) {
|
||||||
routedDefault[v] = route(v)
|
routedDefault[v] = route(v)
|
||||||
}
|
|
||||||
|
|
||||||
routedDefault.isAxiosError = (e: unknown) => {
|
|
||||||
if (handle.useStubs && handle.stubs.isAxiosError) {
|
|
||||||
return handle.stubs.isAxiosError(e)
|
|
||||||
}
|
}
|
||||||
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 {
|
routedDefault.isAxiosError = (e: unknown) => {
|
||||||
...real,
|
for (let i = handles.length - 1; i >= 0; i--) {
|
||||||
...routedDefault,
|
const h = handles[i]
|
||||||
default: routedDefault,
|
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
|
return handle
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user