mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
test: add spread+flag axios mock helper to stop CI mock pollution
Bare `mock.module('axios', () => ({ default: { stubs } }))` is
process-global last-write-wins and drops `axios.create`, `request`,
`isAxiosError`, etc. that real consumers need. In CI's alphabetical
file order, that produces dozens of polluted failures (AgentsPlatformView,
schedule API, memory-stores API, etc.) that don't reproduce on WSL2.
Introduce `tests/mocks/axios.ts` with `setupAxiosMock()` — `require('axios')`
inside the factory, spread real shape, route each verb through a per-suite
`useStubs` flag. beforeAll flips on, afterAll flips off; the spread
fall-through eliminates cross-file leakage.
Refactored 12 axios mockers in tests/, plus the bare `@anthropic/ink` mocks
in ultrareviewCommand and onboarding suites (same pollution pattern broke
AgentsPlatformView's Box/Text rendering).
Verified: 5339/5345 tests pass locally; remaining 6 failures are
pre-existing isolation issues unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,31 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
mock,
|
||||||
|
test,
|
||||||
|
} from 'bun:test'
|
||||||
import { authMock } from '../../../../../../tests/mocks/auth'
|
import { authMock } from '../../../../../../tests/mocks/auth'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
let requestStatus = 200
|
let requestStatus = 200
|
||||||
const auditRecords: Record<string, unknown>[] = []
|
const auditRecords: Record<string, unknown>[] = []
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.request = async () => ({
|
||||||
request: async () => ({
|
status: requestStatus,
|
||||||
status: requestStatus,
|
data: { ok: requestStatus >= 200 && requestStatus < 300 },
|
||||||
data: { ok: requestStatus >= 200 && requestStatus < 300 },
|
})
|
||||||
}),
|
|
||||||
},
|
beforeAll(() => {
|
||||||
}))
|
axiosHandle.useStubs = true
|
||||||
|
})
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
mock.module('src/utils/auth.js', authMock)
|
mock.module('src/utils/auth.js', authMock)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
afterAll,
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
mock,
|
mock,
|
||||||
test,
|
test,
|
||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
// After this suite finishes, switch our getSecret override off so localVault's
|
// After this suite finishes, switch our getSecret override off so localVault's
|
||||||
// own store.test.ts (running in the same process) sees the real impl.
|
// own store.test.ts (running in the same process) sees the real impl. Also
|
||||||
|
// flip the axios stub flag off so the spread mock falls through to real axios
|
||||||
|
// for any test file that runs after this one.
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
useMockForGetSecret = false
|
useMockForGetSecret = false
|
||||||
getSecretShouldThrow = false
|
getSecretShouldThrow = false
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
})
|
})
|
||||||
|
|
||||||
// We mock the LOWER layers (axios + localVault store + http util) rather
|
// We mock the LOWER layers (axios + localVault store + http util) rather
|
||||||
@@ -34,9 +43,8 @@ const mockAxiosRequest = mock(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: { request: mockAxiosRequest },
|
axiosHandle.stubs.request = mockAxiosRequest
|
||||||
}))
|
|
||||||
|
|
||||||
let mockedSecret: string | null = 'XSECRETXX'
|
let mockedSecret: string | null = 'XSECRETXX'
|
||||||
let getSecretShouldThrow = false
|
let getSecretShouldThrow = false
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
import {
|
||||||
|
afterAll,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
mock,
|
||||||
|
test,
|
||||||
|
} from 'bun:test'
|
||||||
import { logMock } from '../../../../../../tests/mocks/log'
|
import { logMock } from '../../../../../../tests/mocks/log'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
type MockAxiosResponse = {
|
type MockAxiosResponse = {
|
||||||
data: ArrayBuffer
|
data: ArrayBuffer
|
||||||
@@ -18,17 +27,12 @@ type MockAxiosError = Error & {
|
|||||||
|
|
||||||
let getMock: (url: string) => Promise<MockAxiosResponse>
|
let getMock: (url: string) => Promise<MockAxiosResponse>
|
||||||
|
|
||||||
mock.module('axios', () => {
|
const axiosHandle = setupAxiosMock()
|
||||||
const axiosMock = {
|
axiosHandle.stubs.get = (url: string) => getMock(url)
|
||||||
get: (url: string) => getMock(url),
|
axiosHandle.stubs.isAxiosError = (error: unknown): boolean =>
|
||||||
isAxiosError: (error: unknown): error is MockAxiosError =>
|
typeof error === 'object' &&
|
||||||
typeof error === 'object' &&
|
error !== null &&
|
||||||
error !== null &&
|
(error as { isAxiosError?: unknown }).isAxiosError === true
|
||||||
(error as { isAxiosError?: unknown }).isAxiosError === true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return { default: axiosMock }
|
|
||||||
})
|
|
||||||
|
|
||||||
mock.module('src/services/analytics/index.js', () => ({
|
mock.module('src/services/analytics/index.js', () => ({
|
||||||
logEvent: () => {},
|
logEvent: () => {},
|
||||||
@@ -67,6 +71,14 @@ beforeEach(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
describe('WebFetch response headers', () => {
|
describe('WebFetch response headers', () => {
|
||||||
test('reads redirect Location from AxiosHeaders-style get()', async () => {
|
test('reads redirect Location from AxiosHeaders-style get()', async () => {
|
||||||
getMock = async () => {
|
getMock = async () => {
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { describe, expect, mock, test } from 'bun:test'
|
import { afterAll, describe, expect, mock, test } from 'bun:test'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
|
// Each test below calls `mock.module('axios', ...)` per-test. Re-register a
|
||||||
|
// spread-real axios mock at end-of-file so the per-test stubs do not leak
|
||||||
|
// into subsequent test files (mock.module is process-global, last-write-wins).
|
||||||
|
afterAll(() => {
|
||||||
|
setupAxiosMock()
|
||||||
|
})
|
||||||
|
|
||||||
const _abortMock = () => ({
|
const _abortMock = () => ({
|
||||||
AbortError: class AbortError extends Error {
|
AbortError: class AbortError extends Error {
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
mock,
|
||||||
|
test,
|
||||||
|
} from 'bun:test'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
|
// Each test below calls `mock.module('axios', ...)` per-test. Without an
|
||||||
|
// afterAll cleanup, the LAST per-test stub leaks into every test file that
|
||||||
|
// runs after this one (mock.module is process-global, last-write-wins). The
|
||||||
|
// spread-real mock registered here at the end re-routes axios to the real
|
||||||
|
// module, undoing the stub leakage so later suites see real axios.
|
||||||
|
afterAll(() => {
|
||||||
|
setupAxiosMock()
|
||||||
|
})
|
||||||
|
|
||||||
// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
|
// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
|
||||||
// src/* path alias resolution. Provide AbortError directly so the dynamic
|
// src/* path alias resolution. Provide AbortError directly so the dynamic
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
import { afterAll, afterEach, describe, expect, mock, test } from 'bun:test'
|
||||||
|
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||||
|
|
||||||
|
// Each test below calls `mock.module('axios', ...)` per-test. Re-register a
|
||||||
|
// spread-real axios mock at end-of-file so the per-test stubs do not leak
|
||||||
|
// into subsequent test files (mock.module is process-global, last-write-wins).
|
||||||
|
afterAll(() => {
|
||||||
|
setupAxiosMock()
|
||||||
|
})
|
||||||
|
|
||||||
const _abortMock = () => ({
|
const _abortMock = () => ({
|
||||||
AbortError: class AbortError extends Error {
|
AbortError: class AbortError extends Error {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
// Mock side-effect modules first
|
// Mock side-effect modules first
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
@@ -48,15 +50,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Lazy import after mocks are in place
|
// Lazy import after mocks are in place
|
||||||
let listAgents: typeof import('../agentsApi.js').listAgents
|
let listAgents: typeof import('../agentsApi.js').listAgents
|
||||||
@@ -65,6 +63,7 @@ let deleteAgent: typeof import('../agentsApi.js').deleteAgent
|
|||||||
let runAgent: typeof import('../agentsApi.js').runAgent
|
let runAgent: typeof import('../agentsApi.js').runAgent
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../agentsApi.js')
|
const mod = await import('../agentsApi.js')
|
||||||
listAgents = mod.listAgents
|
listAgents = mod.listAgents
|
||||||
createAgent = mod.createAgent
|
createAgent = mod.createAgent
|
||||||
@@ -72,6 +71,10 @@ beforeAll(async () => {
|
|||||||
runAgent = mod.runAgent
|
runAgent = mod.runAgent
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -60,16 +62,12 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.patch = axiosPatchMock
|
||||||
patch: axiosPatchMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||||
let listStores: typeof import('../memoryStoresApi.js').listStores
|
let listStores: typeof import('../memoryStoresApi.js').listStores
|
||||||
@@ -85,6 +83,7 @@ let listVersions: typeof import('../memoryStoresApi.js').listVersions
|
|||||||
let redactVersion: typeof import('../memoryStoresApi.js').redactVersion
|
let redactVersion: typeof import('../memoryStoresApi.js').redactVersion
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../memoryStoresApi.js')
|
const mod = await import('../memoryStoresApi.js')
|
||||||
listStores = mod.listStores
|
listStores = mod.listStores
|
||||||
getStore = mod.getStore
|
getStore = mod.getStore
|
||||||
@@ -99,6 +98,10 @@ beforeAll(async () => {
|
|||||||
redactVersion = mod.redactVersion
|
redactVersion = mod.redactVersion
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { logMock } from '../../../../tests/mocks/log';
|
import { logMock } from '../../../../tests/mocks/log';
|
||||||
import { debugMock } from '../../../../tests/mocks/debug';
|
import { debugMock } from '../../../../tests/mocks/debug';
|
||||||
|
|
||||||
|
// Pre-import real ink so we can fall through after this suite. Bun's
|
||||||
|
// mock.module is process-global / last-write-wins; without delegation the
|
||||||
|
// stub Box/Pane/Text/useTheme leak into other test files (e.g.
|
||||||
|
// AgentsPlatformView.test.tsx) that need real ink components.
|
||||||
|
const _realOnboardingInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||||
|
let _useStubInkForOnboarding = true;
|
||||||
|
afterAll(() => {
|
||||||
|
_useStubInkForOnboarding = false;
|
||||||
|
});
|
||||||
|
|
||||||
mock.module('bun:bundle', () => ({
|
mock.module('bun:bundle', () => ({
|
||||||
feature: (_name: string) => false,
|
feature: (_name: string) => false,
|
||||||
}));
|
}));
|
||||||
@@ -37,13 +47,20 @@ mock.module('src/utils/config.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Stub heavy theme + ink imports — the launcher only references them for
|
// Stub heavy theme + ink imports — the launcher only references them for
|
||||||
// the `theme` subcommand JSX render path.
|
// the `theme` subcommand JSX render path. Spread real ink so when the flag
|
||||||
mock.module('@anthropic/ink', () => ({
|
// flips off in afterAll, later test files see real components.
|
||||||
Box: ({ children }: { children?: React.ReactNode }) => React.createElement('box', null, children),
|
mock.module('@anthropic/ink', () => {
|
||||||
Pane: ({ children }: { children?: React.ReactNode }) => React.createElement('pane', null, children),
|
if (_useStubInkForOnboarding) {
|
||||||
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('text', null, children),
|
return {
|
||||||
useTheme: () => ['dark', (_t: string) => undefined],
|
..._realOnboardingInkMod,
|
||||||
}));
|
Box: ({ children }: { children?: React.ReactNode }) => React.createElement('box', null, children),
|
||||||
|
Pane: ({ children }: { children?: React.ReactNode }) => React.createElement('pane', null, children),
|
||||||
|
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('text', null, children),
|
||||||
|
useTheme: () => ['dark', (_t: string) => undefined],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _realOnboardingInkMod;
|
||||||
|
});
|
||||||
|
|
||||||
mock.module('src/components/ThemePicker.js', () => ({
|
mock.module('src/components/ThemePicker.js', () => ({
|
||||||
ThemePicker: () => React.createElement('theme-picker'),
|
ThemePicker: () => React.createElement('theme-picker'),
|
||||||
|
|||||||
@@ -15,17 +15,26 @@
|
|||||||
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js';
|
import { debugMock } from '../../../../tests/mocks/debug.js';
|
||||||
import { logMock } from '../../../../tests/mocks/log.js';
|
import { logMock } from '../../../../tests/mocks/log.js';
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js';
|
||||||
|
|
||||||
// Pre-import the real react module so we can delegate after this suite.
|
// Pre-import the real react and ink modules so we can delegate after this
|
||||||
// Bun's mock.module is process-global / last-write-wins; without delegation
|
// suite. Bun's mock.module is process-global / last-write-wins; without
|
||||||
// the stub createElement leaks into other test files (e.g.
|
// delegation the stub createElement / stub ink components leak into other
|
||||||
// SnapshotUpdateDialog.test.tsx) that need real React.createElement.
|
// test files (e.g. SnapshotUpdateDialog.test.tsx, AgentsPlatformView.test.tsx)
|
||||||
|
// that need real React.createElement and real Box/Text components.
|
||||||
const _realReactMod = (await import('react')) as Record<string, unknown> & {
|
const _realReactMod = (await import('react')) as Record<string, unknown> & {
|
||||||
default?: Record<string, unknown>;
|
default?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
const _realInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||||
let _useStubReactForUltrareview = true;
|
let _useStubReactForUltrareview = true;
|
||||||
|
let _useStubInkForUltrareview = true;
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
_useStubReactForUltrareview = false;
|
_useStubReactForUltrareview = false;
|
||||||
|
_useStubInkForUltrareview = false;
|
||||||
|
// The handle reference exists by the time afterAll runs (TDZ resolves via
|
||||||
|
// closure). Flip useStubs off so the spread-real fall-through kicks in for
|
||||||
|
// any test file that runs after this one in the same process.
|
||||||
|
_ultrareviewAxiosHandle.useStubs = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock dependency chain before any subject import
|
// Mock dependency chain before any subject import
|
||||||
@@ -79,14 +88,15 @@ const mockAxiosPost = mock(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
mock.module('axios', () => {
|
// Spread real axios + flag-gate stubs so the per-test mockAxiosPost stops
|
||||||
const axiosMock = {
|
// leaking into later test files (mock.module is process-global). Default ON
|
||||||
post: mockAxiosPost,
|
// for this suite; afterAll above flips _useStubReactForUltrareview, but here
|
||||||
isAxiosError: (e: unknown) =>
|
// we tie axios cleanup to the helper's own flag — see suite-level afterAll.
|
||||||
typeof e === 'object' && e !== null && (e as { isAxiosError?: boolean }).isAxiosError === true,
|
const _ultrareviewAxiosHandle = setupAxiosMock();
|
||||||
};
|
_ultrareviewAxiosHandle.useStubs = true;
|
||||||
return { default: axiosMock, ...axiosMock };
|
_ultrareviewAxiosHandle.stubs.post = mockAxiosPost;
|
||||||
});
|
_ultrareviewAxiosHandle.stubs.isAxiosError = (e: unknown) =>
|
||||||
|
typeof e === 'object' && e !== null && (e as { isAxiosError?: boolean }).isAxiosError === true;
|
||||||
|
|
||||||
// Mock detectCurrentRepositoryWithHost
|
// Mock detectCurrentRepositoryWithHost
|
||||||
mock.module('src/utils/detectRepository.js', () => ({
|
mock.module('src/utils/detectRepository.js', () => ({
|
||||||
@@ -128,11 +138,21 @@ mock.module('react', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
mock.module('@anthropic/ink', () => ({
|
// Spread real ink + flag-gate the stub components. Without spread, the bare
|
||||||
Box: 'Box',
|
// { Box: 'Box', Dialog: 'Dialog', Text: 'Text' } leaks into every later test
|
||||||
Dialog: 'Dialog',
|
// file (e.g. AgentsPlatformView.test.tsx) that imports @anthropic/ink — those
|
||||||
Text: 'Text',
|
// consumers receive strings instead of real components and rendering breaks.
|
||||||
}));
|
mock.module('@anthropic/ink', () => {
|
||||||
|
if (_useStubInkForUltrareview) {
|
||||||
|
return {
|
||||||
|
..._realInkMod,
|
||||||
|
Box: 'Box',
|
||||||
|
Dialog: 'Dialog',
|
||||||
|
Text: 'Text',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _realInkMod;
|
||||||
|
});
|
||||||
|
|
||||||
mock.module('src/components/CustomSelect/select.js', () => ({
|
mock.module('src/components/CustomSelect/select.js', () => ({
|
||||||
Select: 'Select',
|
Select: 'Select',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -57,15 +59,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||||
// Use the src/ alias path (same canonical key used in launchSchedule.test.ts mock)
|
// Use the src/ alias path (same canonical key used in launchSchedule.test.ts mock)
|
||||||
@@ -79,6 +77,7 @@ let deleteTrigger: typeof import('../triggersApi.js').deleteTrigger
|
|||||||
let runTrigger: typeof import('../triggersApi.js').runTrigger
|
let runTrigger: typeof import('../triggersApi.js').runTrigger
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../triggersApi.js')
|
const mod = await import('../triggersApi.js')
|
||||||
listTriggers = mod.listTriggers
|
listTriggers = mod.listTriggers
|
||||||
getTrigger = mod.getTrigger
|
getTrigger = mod.getTrigger
|
||||||
@@ -88,6 +87,10 @@ beforeAll(async () => {
|
|||||||
runTrigger = mod.runTrigger
|
runTrigger = mod.runTrigger
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -62,15 +64,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||||
let listSkills: typeof import('../skillsApi.js').listSkills
|
let listSkills: typeof import('../skillsApi.js').listSkills
|
||||||
@@ -81,6 +79,7 @@ let createSkill: typeof import('../skillsApi.js').createSkill
|
|||||||
let deleteSkill: typeof import('../skillsApi.js').deleteSkill
|
let deleteSkill: typeof import('../skillsApi.js').deleteSkill
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../skillsApi.js')
|
const mod = await import('../skillsApi.js')
|
||||||
listSkills = mod.listSkills
|
listSkills = mod.listSkills
|
||||||
getSkill = mod.getSkill
|
getSkill = mod.getSkill
|
||||||
@@ -90,6 +89,10 @@ beforeAll(async () => {
|
|||||||
deleteSkill = mod.deleteSkill
|
deleteSkill = mod.deleteSkill
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -73,15 +74,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── fs/promises mock ─────────────────────────────────────────────────────────
|
// ── fs/promises mock ─────────────────────────────────────────────────────────
|
||||||
// Bun's mock.module is global per-process and last-write-wins. Replacing
|
// Bun's mock.module is global per-process and last-write-wins. Replacing
|
||||||
@@ -119,6 +116,7 @@ let getClaudeConfigHomeDir: typeof import('../../../utils/envUtils.js').getClaud
|
|||||||
let origConfigDir: string | undefined
|
let origConfigDir: string | undefined
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../launchSkillStore.js')
|
const mod = await import('../launchSkillStore.js')
|
||||||
callSkillStore = mod.callSkillStore
|
callSkillStore = mod.callSkillStore
|
||||||
const envMod = await import('../../../utils/envUtils.js')
|
const envMod = await import('../../../utils/envUtils.js')
|
||||||
@@ -130,6 +128,7 @@ beforeAll(async () => {
|
|||||||
// Flip the stub flag off after this suite so localVault/store and other
|
// Flip the stub flag off after this suite so localVault/store and other
|
||||||
// fs-dependent tests in the same process see real readFile/readdir/etc.
|
// fs-dependent tests in the same process see real readFile/readdir/etc.
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
useSkillStoreFsStubs = false
|
useSkillStoreFsStubs = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -60,15 +62,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosHandle = setupAxiosMock()
|
||||||
default: {
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
get: axiosGetMock,
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
delete: axiosDeleteMock,
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
},
|
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||||
let listVaults: typeof import('../vaultsApi.js').listVaults
|
let listVaults: typeof import('../vaultsApi.js').listVaults
|
||||||
@@ -80,6 +78,7 @@ let addCredential: typeof import('../vaultsApi.js').addCredential
|
|||||||
let archiveCredential: typeof import('../vaultsApi.js').archiveCredential
|
let archiveCredential: typeof import('../vaultsApi.js').archiveCredential
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../vaultsApi.js')
|
const mod = await import('../vaultsApi.js')
|
||||||
listVaults = mod.listVaults
|
listVaults = mod.listVaults
|
||||||
createVault = mod.createVault
|
createVault = mod.createVault
|
||||||
@@ -90,6 +89,10 @@ beforeAll(async () => {
|
|||||||
archiveCredential = mod.archiveCredential
|
archiveCredential = mod.archiveCredential
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
afterAll,
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeAll,
|
beforeAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
} from 'bun:test'
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
mock.module('src/utils/log.ts', logMock)
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -51,24 +53,27 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
const axiosDeleteMock = mock(async () => ({}))
|
||||||
default: {
|
|
||||||
get: axiosGetMock,
|
const axiosHandle = setupAxiosMock()
|
||||||
post: axiosPostMock,
|
axiosHandle.stubs.get = axiosGetMock
|
||||||
delete: mock(async () => ({})),
|
axiosHandle.stubs.post = axiosPostMock
|
||||||
isAxiosError: axiosIsAxiosError,
|
axiosHandle.stubs.delete = axiosDeleteMock
|
||||||
},
|
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||||
isAxiosError: axiosIsAxiosError,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||||
let callVault: typeof import('../launchVault.js').callVault
|
let callVault: typeof import('../launchVault.js').callVault
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
const mod = await import('../launchVault.js')
|
const mod = await import('../launchVault.js')
|
||||||
callVault = mod.callVault
|
callVault = mod.callVault
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosGetMock.mockClear()
|
axiosGetMock.mockClear()
|
||||||
axiosPostMock.mockClear()
|
axiosPostMock.mockClear()
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
* Verifies all three action enum states (proceed/confirm/blocked),
|
* Verifies all three action enum states (proceed/confirm/blocked),
|
||||||
* network/HTTP error handling, and Zod schema mismatch fallback.
|
* network/HTTP error handling, and Zod schema mismatch fallback.
|
||||||
*/
|
*/
|
||||||
import { describe, expect, mock, test } from 'bun:test'
|
import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
import { logMock } from '../../../../tests/mocks/log.js'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
// Mock dependency chain before any subject import
|
// Mock dependency chain before any subject import
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
@@ -46,15 +47,19 @@ const mockAxiosPost = mock(async (..._args: any[]): Promise<any> => {
|
|||||||
throw new Error('not configured')
|
throw new Error('not configured')
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module('axios', () => {
|
const axiosHandle = setupAxiosMock()
|
||||||
const axiosMock = {
|
axiosHandle.stubs.post = mockAxiosPost
|
||||||
post: mockAxiosPost,
|
axiosHandle.stubs.isAxiosError = (e: unknown) =>
|
||||||
isAxiosError: (e: unknown) =>
|
typeof e === 'object' &&
|
||||||
typeof e === 'object' &&
|
e !== null &&
|
||||||
e !== null &&
|
(e as { isAxiosError?: boolean }).isAxiosError === true
|
||||||
(e as { isAxiosError?: boolean }).isAxiosError === true,
|
|
||||||
}
|
beforeAll(() => {
|
||||||
return { default: axiosMock, ...axiosMock }
|
axiosHandle.useStubs = true
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
})
|
})
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { mock, describe, expect, test, afterEach } from 'bun:test'
|
import {
|
||||||
|
mock,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
test,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
afterAll,
|
||||||
|
} from 'bun:test'
|
||||||
import { debugMock } from '../../../../tests/mocks/debug'
|
import { debugMock } from '../../../../tests/mocks/debug'
|
||||||
|
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||||
|
|
||||||
|
const axiosHandle = setupAxiosMock()
|
||||||
|
axiosHandle.stubs.get = async () => ({ data: { servers: [] } })
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
axiosHandle.useStubs = true
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
axiosHandle.useStubs = false
|
||||||
|
})
|
||||||
|
|
||||||
mock.module('axios', () => ({
|
|
||||||
default: { get: async () => ({ data: { servers: [] } }) },
|
|
||||||
}))
|
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
|
|
||||||
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
||||||
|
|||||||
141
tests/mocks/axios.ts
Normal file
141
tests/mocks/axios.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Shared axios mock helper using the spread+flag pattern.
|
||||||
|
*
|
||||||
|
* Why this exists:
|
||||||
|
* `mock.module('axios', () => ({ default: { get, post } }))` is process-global
|
||||||
|
* (last-write-wins) and drops real axios shape (`create`, `request`, `isAxiosError`,
|
||||||
|
* verb methods, etc). When test file A registers a stub-only mock, every later
|
||||||
|
* test file B that imports axios gets A's bare stub even after A finishes —
|
||||||
|
* unless B registers its own mock. In CI (alphabetical file order on Linux),
|
||||||
|
* that produces dozens of "polluted" failures that don't reproduce on WSL2.
|
||||||
|
*
|
||||||
|
* The spread+flag pattern fixes both problems:
|
||||||
|
* 1. `require('axios')` INSIDE the factory pulls the real module (top-level
|
||||||
|
* `await import('axios')` would re-enter the mocked one and recurse).
|
||||||
|
* 2. The factory spreads the real exports, then replaces method references
|
||||||
|
* with router functions that read a per-suite `useStubs` boolean. When the
|
||||||
|
* flag is OFF (default), calls fall through to the real axios method;
|
||||||
|
* when ON, they hit the suite's stubs. Each suite flips the flag in
|
||||||
|
* beforeAll and clears it in afterAll, so cross-suite pollution disappears.
|
||||||
|
*
|
||||||
|
* Usage in a test file:
|
||||||
|
*
|
||||||
|
* import { setupAxiosMock } from '../../../tests/mocks/axios'
|
||||||
|
*
|
||||||
|
* const axiosHandle = setupAxiosMock()
|
||||||
|
* axiosHandle.stubs.get = (url, config) => Promise.resolve({ status: 200, data: {...}, headers: {}, statusText: 'OK', config })
|
||||||
|
* axiosHandle.stubs.post = ...
|
||||||
|
*
|
||||||
|
* beforeAll(() => { axiosHandle.useStubs = true })
|
||||||
|
* afterAll(() => { axiosHandle.useStubs = false })
|
||||||
|
*
|
||||||
|
* If your suite needs an `isAxiosError` predicate that recognises plain
|
||||||
|
* 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. —
|
||||||
|
// and assigning them to a tighter signature like `(...args: unknown[]) => unknown`
|
||||||
|
// triggers TS2322 (parameter type contravariance). The biome rule that
|
||||||
|
// disallows `any` here is already disabled project-wide, so plain `any` is
|
||||||
|
// the correct escape hatch for an internal test-only union.
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: see comment above
|
||||||
|
type AnyFn = (...args: any[]) => unknown
|
||||||
|
|
||||||
|
export type AxiosMethodStubs = {
|
||||||
|
get?: AnyFn
|
||||||
|
post?: AnyFn
|
||||||
|
put?: AnyFn
|
||||||
|
patch?: AnyFn
|
||||||
|
delete?: AnyFn
|
||||||
|
head?: AnyFn
|
||||||
|
options?: AnyFn
|
||||||
|
request?: AnyFn
|
||||||
|
isAxiosError?: (e: unknown) => boolean
|
||||||
|
isCancel?: (e: unknown) => boolean
|
||||||
|
create?: AnyFn
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AxiosMockHandle = {
|
||||||
|
/** When true, calls are routed to `stubs`; when false, to real axios. */
|
||||||
|
useStubs: boolean
|
||||||
|
/** Per-method stubs. Only set the methods your suite exercises. */
|
||||||
|
stubs: AxiosMethodStubs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function setupAxiosMock(): AxiosMockHandle {
|
||||||
|
const handle: AxiosMockHandle = { useStubs: false, stubs: {} }
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
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[]) => {
|
||||||
|
if (handle.useStubs) {
|
||||||
|
const stub = handle.stubs[method] as AnyFn | undefined
|
||||||
|
if (stub) return stub(...args)
|
||||||
|
}
|
||||||
|
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 routedDefault: Record<string, unknown> = { ...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 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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return handle
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user