import { beforeEach, describe, expect, it, vi } from 'vitest' const requestUseMock = vi.fn() const responseUseMock = vi.fn() const axiosGetMock = vi.fn() const axiosPutMock = vi.fn() const axiosPostMock = vi.fn() const axiosMock = vi.fn() const progressStartMock = vi.fn() const progressDoneMock = vi.fn() const alertMock = vi.fn() const messageErrorMock = vi.fn() const storageGetItemMock = vi.fn((name: string, type?: string) => { if (name === 'x-auth-token') return 'mock-token' if (name === 'language' && type === 'localStorage') return 'en' return undefined }) vi.mock('axios', () => { const axios = Object.assign(axiosMock, { defaults: { baseURL: '', timeout: 0, headers: { post: {} } }, interceptors: { request: { use: requestUseMock }, response: { use: responseUseMock } }, get: axiosGetMock, put: axiosPutMock, post: axiosPostMock }) return { default: axios } }) vi.mock('nprogress', () => ({ default: { start: progressStartMock, done: progressDoneMock } })) vi.mock('element-plus', () => ({ ElMessageBox: { alert: alertMock }, ElMessage: { error: messageErrorMock } })) vi.mock('../../../script/storage', () => ({ __getItem: storageGetItemMock })) vi.mock('../../../i18n', () => ({ i18n: { global: { t: (key: string) => `i18n:${key}` } } })) describe('Http', () => { const importHttp = async () => { vi.resetModules() return import('../../../script/http') } beforeEach(() => { vi.clearAllMocks() window.sessionStorage.clear() window.localStorage.clear() window.history.replaceState({}, '', '/resource/list') Object.defineProperty(window.navigator, 'onLine', { configurable: true, value: true }) }) it('模块初始化时会设置 axios 默认配置并注册拦截器', async () => { await importHttp() const axios = (await import('axios')).default as any expect(axios.defaults.baseURL).toBe('/api') expect(axios.defaults.timeout).toBe(1000000) expect(axios.defaults.headers.post['Content-Type']).toBe('application/json;charset=UTF-8') expect(requestUseMock).toHaveBeenCalledTimes(1) expect(responseUseMock).toHaveBeenCalledTimes(1) }) it('请求拦截器会注入 token、语言和 AUTH-ROUTE,并调用 Http.request', async () => { window.sessionStorage.setItem('serveConfig', JSON.stringify({ token: 'authKey' })) const { Http } = await importHttp() const requestSpy = vi.spyOn(Http, 'request') const interceptor = requestUseMock.mock.calls[0][0] const config: any = { method: 'get', url: '/users', headers: {} } const result = interceptor(config) expect(result.headers.authKey).toBe('mock-token') expect(result.headers['accept-language']).toBe('en-US') expect(result.headers['AUTH-ROUTE']).toBe('/resource/list') expect(requestSpy).toHaveBeenCalledWith(result) }) it('响应拦截器成功分支会处理 code=111 并调用 Http.response', async () => { const { Http } = await importHttp() const responseSpy = vi.spyOn(Http, 'response') const interceptor = responseUseMock.mock.calls[0][0] const response = interceptor({ data: { code: 111 } }) expect(window.sessionStorage.getItem('token')).toBe('') expect(responseSpy).toHaveBeenCalledWith({ data: { code: 111 } }) expect(response).toEqual({ data: { code: 111 } }) }) it('响应拦截器遇到 401 时只弹一次登录失效提示', async () => { window.sessionStorage.setItem( 'serveConfig', JSON.stringify({ logoutPrompt: '请重新登录', defaultLoginHref: '/login' }) ) await importHttp() const errorInterceptor = responseUseMock.mock.calls[0][1] errorInterceptor({ response: { status: 401 } }) errorInterceptor({ response: { status: 401 } }) expect(alertMock).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledWith( '请重新登录', 'i18n:confirmTips', expect.objectContaining({ confirmButtonText: 'i18n:confirm' }) ) }) it('离线时响应拦截器会提示网络错误', async () => { Object.defineProperty(window.navigator, 'onLine', { configurable: true, value: false }) await importHttp() const errorInterceptor = responseUseMock.mock.calls[0][1] const result = errorInterceptor({ message: 'offline' }) expect(messageErrorMock).toHaveBeenCalledWith({ message: 'i18n:networkRequestError' }) expect(result).toBeUndefined() }) it('get 会使用 serveConfig.baseURL 发起请求并正确维护进度条', async () => { window.sessionStorage.setItem('serveConfig', JSON.stringify({ baseURL: '/gateway' })) const response = { data: { list: [1] } } axiosGetMock.mockResolvedValue(response) const { Http } = await importHttp() await expect(Http.get('/users', { page: 1 }, false, { headers: { trace: '1' } })).resolves.toBe(response) expect(progressStartMock).toHaveBeenCalledTimes(1) expect(progressDoneMock).toHaveBeenCalledTimes(1) expect(((await import('axios')).default as any).defaults.baseURL).toBe('/gateway') expect(axiosGetMock).toHaveBeenCalledWith('/users', { params: { page: 1 }, headers: { trace: '1' } }) }) it('post 会在 noBase=true 时清空 baseURL,并把对象参数序列化后提交', async () => { const response = { data: { ok: true } } axiosPostMock.mockResolvedValue(response) const { Http } = await importHttp() await expect(Http.post('/save', { id: 1 }, true, { timeout: 2000 })).resolves.toBe(response) expect(((await import('axios')).default as any).defaults.baseURL).toBe('') expect(axiosPostMock).toHaveBeenCalledWith('/save', '{"id":1}', { timeout: 2000 }) expect(progressStartMock).toHaveBeenCalledTimes(1) expect(progressDoneMock).toHaveBeenCalledTimes(1) }) it('delete 与 upload 会按约定透传参数', async () => { const { Http } = await importHttp() const deleteResp = { data: { deleted: true } } const uploadResp = { data: { uploaded: true } } axiosMock.mockResolvedValue(deleteResp) axiosPostMock.mockResolvedValue(uploadResp) await expect(Http.delete('/users', { id: 1 }, false, { headers: { trace: '2' } })).resolves.toBe(deleteResp) await expect(Http.upload('/upload', 'file-data', { timeout: 5000 })).resolves.toBe(uploadResp) expect(axiosMock).toHaveBeenCalledWith({ method: 'delete', url: '/users', data: { id: 1 }, headers: { trace: '2' } }) expect(axiosPostMock).toHaveBeenCalledWith('/upload', 'file-data', { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 5000 }) }) it('download 解析到失败响应时会提示错误并移除 iframe', async () => { await importHttp() const { Http } = await import('../../../script/http') const appendChild = vi.spyOn(document.body, 'appendChild') const removeChild = vi.spyOn(document.body, 'removeChild') const iframe = document.createElement('iframe') Object.defineProperty(iframe, 'contentWindow', { configurable: true, value: { document: { querySelector: vi.fn(() => ({ innerHTML: JSON.stringify({ ok: false, message: '下载失败' }) })) } } }) vi.spyOn(document, 'createElement').mockImplementation(((tagName: string) => { if (tagName === 'iframe') { return iframe } return document.createElement(tagName) }) as typeof document.createElement) Http.download('/download/file') iframe.onload?.(new Event('load')) expect(appendChild).toHaveBeenCalledWith(iframe) expect(iframe.src).toContain('/download/file') expect(messageErrorMock).toHaveBeenCalledWith('下载失败') expect(removeChild).toHaveBeenCalledWith(iframe) }) })