From ee588f694185180c4a5ff69fc2b96de973331617 Mon Sep 17 00:00:00 2001 From: zjx0905 <954270063@qq.com> Date: Sun, 9 Apr 2023 21:01:43 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B5=8B=E8=AF=95=20dispatchRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/test.utils.ts | 12 +- src/adapter.ts | 21 ++-- src/core/Axios.ts | 20 ++-- src/core/dispatchRequest.ts | 20 +++- src/core/request.ts | 14 +-- src/index.ts | 3 + test/core/dispatchRequest.test.ts | 189 +++++++++++++++++++++++++++++- test/core/request.test.ts | 98 +++++++++++++--- 8 files changed, 320 insertions(+), 57 deletions(-) diff --git a/scripts/test.utils.ts b/scripts/test.utils.ts index 0c2e0c9..1a18e0e 100644 --- a/scripts/test.utils.ts +++ b/scripts/test.utils.ts @@ -50,11 +50,19 @@ export interface MockAdapterOptions { after?: () => void; } -export function mockAdapterBase( +function mockAdapterBase( type: 'success' | 'error' | 'fail' = 'success', options: MockAdapterOptions = {}, ) { - const { headers = {}, data = {}, delay = 0, before, after } = options; + const { + headers = {}, + data = { + result: null, + }, + delay = 0, + before, + after, + } = options; return (config: AxiosAdapterRequestConfig) => { let canceled = false; diff --git a/src/adapter.ts b/src/adapter.ts index df5fc9a..3859092 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -23,6 +23,8 @@ export type AxiosAdapterRequestMethod = | 'TRACE' | 'CONNECT'; +export type AxiosAdapterResponseData = string | ArrayBuffer | AnyObject; + export interface AxiosAdapterResponse extends AnyObject { /** * 状态码 @@ -39,7 +41,7 @@ export interface AxiosAdapterResponse extends AnyObject { /** * 响应数据 */ - data: string | ArrayBuffer | AnyObject; + data: AxiosAdapterResponseData; } export interface AxiosAdapterResponseError extends AnyObject { @@ -139,17 +141,20 @@ export interface AxiosPlatform { download: AxiosAdapterDownload; } -export type AxiosAdapterTask = { - abort?(): void; - onProgressUpdate?(callback: AxiosProgressCallback): void; - offProgressUpdate?(callback: AxiosProgressCallback): void; -} | void; +export type AxiosAdapterTask = + | undefined + | void + | { + abort?(): void; + onProgressUpdate?(callback: AxiosProgressCallback): void; + offProgressUpdate?(callback: AxiosProgressCallback): void; + }; export interface AxiosAdapter { (config: AxiosAdapterRequestConfig): AxiosAdapterTask; } -export function getAdapterDefault(): AxiosAdapter | undefined { +export function getAdapterDefault() { const tryGetPlatforms = [ () => uni, () => wx, @@ -182,7 +187,7 @@ export function getAdapterDefault(): AxiosAdapter | undefined { return createAdapter(platform); } -export function createAdapter(platform: AxiosPlatform): AxiosAdapter { +export function createAdapter(platform: AxiosPlatform) { assert(isPlainObject(platform), 'platform 不是一个 object'); assert(isFunction(platform.request), 'request 不是一个 function'); assert(isFunction(platform.upload), 'upload 不是一个 function'); diff --git a/src/core/Axios.ts b/src/core/Axios.ts index dca506e..659f887 100644 --- a/src/core/Axios.ts +++ b/src/core/Axios.ts @@ -1,7 +1,6 @@ import { buildURL } from '../helpers/buildURL'; import { isAbsoluteURL } from '../helpers/isAbsoluteURL'; import { combineURL } from '../helpers/combineURL'; -import { mergeConfig } from './mergeConfig'; import { AxiosAdapter, AxiosAdapterRequestMethod, @@ -9,12 +8,14 @@ import { AxiosAdapterResponse, AxiosAdapterRequestConfig, AxiosAdapterResponseError, + AxiosAdapterResponseData, } from '../adapter'; +import { mergeConfig } from './mergeConfig'; import { CancelToken } from './cancel'; import { dispatchRequest } from './dispatchRequest'; -import InterceptorManager from './InterceptorManager'; import { AxiosTransformer } from './transformData'; import AxiosDomain from './AxiosDomain'; +import InterceptorManager from './InterceptorManager'; export type AxiosRequestMethod = | AxiosAdapterRequestMethod @@ -79,12 +80,7 @@ export interface AxiosRequestFormData extends AnyObject { export type AxiosRequestData = AnyObject | AxiosRequestFormData; -export type AxiosResponseData = - | undefined - | number - | string - | ArrayBuffer - | AnyObject; +export type AxiosResponseData = undefined | number | AxiosAdapterResponseData; export interface AxiosProgressEvent { progress: number; @@ -151,7 +147,7 @@ export interface AxiosRequestConfig /** * 异常处理 */ - errorHandler?: (error: unknown) => Promise; + errorHandler?: (error: unknown) => Promise | void; /** * 监听上传进度 */ @@ -241,10 +237,8 @@ export default class Axios extends AxiosDomain { if (!isAbsoluteURL(baseURL)) { defaults.baseURL = combineURL(this.defaults.baseURL ?? '', baseURL); } - return new AxiosDomain( - mergeConfig(this.defaults, defaults), - - (config) => this.#processRequest(config), + return new AxiosDomain(mergeConfig(this.defaults, defaults), (config) => + this.#processRequest(config), ); } diff --git a/src/core/dispatchRequest.ts b/src/core/dispatchRequest.ts index d3eb913..9d25b11 100644 --- a/src/core/dispatchRequest.ts +++ b/src/core/dispatchRequest.ts @@ -1,4 +1,5 @@ -import { isFunction } from '../helpers/isTypes'; +import { isFunction, isString } from '../helpers/isTypes'; +import { assert } from '../helpers/error'; import { isCancel, isCancelToken } from './cancel'; import { flattenHeaders } from './flattenHeaders'; import { AxiosTransformer, transformData } from './transformData'; @@ -17,17 +18,19 @@ function throwIfCancellationRequested(config: AxiosRequestConfig) { export function dispatchRequest(config: AxiosRequestConfig) { throwIfCancellationRequested(config); - const { transformRequest, transformResponse } = config; + assert(isFunction(config.adapter), 'adapter 不是一个 function'); + assert(isString(config.url), 'url 不是一个 string'); + assert(isString(config.method), 'method 不是一个 string'); + + const { errorHandler, transformRequest, transformResponse } = config; config.url = transformURL(config); - config.method = config.method ?? 'get'; config.headers = flattenHeaders(config); transformer(config, transformRequest); function onSuccess(response: AxiosResponse) { throwIfCancellationRequested(config); - transformer(response, transformResponse); return response; } @@ -41,8 +44,13 @@ export function dispatchRequest(config: AxiosRequestConfig) { } } - if (isFunction(config.errorHandler)) { - return config.errorHandler(reason); + if (isFunction(errorHandler)) { + const promise = errorHandler(reason); + if (promise) { + return promise.then(() => { + throw reason; + }); + } } return Promise.reject(reason); diff --git a/src/core/request.ts b/src/core/request.ts index 9e68bde..5ca93ed 100644 --- a/src/core/request.ts +++ b/src/core/request.ts @@ -1,5 +1,4 @@ -import { isFunction, isPlainObject, isString } from '../helpers/isTypes'; -import { assert } from '../helpers/error'; +import { isFunction, isPlainObject } from '../helpers/isTypes'; import { AxiosAdapterRequestConfig, AxiosAdapterRequestMethod, @@ -10,7 +9,6 @@ import { AxiosProgressCallback, AxiosRequestConfig, AxiosResponse, - AxiosResponseData, AxiosResponseError, } from './Axios'; import { isCancelToken } from './cancel'; @@ -41,19 +39,18 @@ function tryToggleProgressUpdate( export function request(config: AxiosRequestConfig) { return new Promise((resolve, reject) => { - assert(isFunction(config.adapter), 'adapter 不是一个 function'); - assert(isString(config.url), 'url 不是一个 string'); + const { adapter, url, method, cancelToken } = config; const adapterConfig: AxiosAdapterRequestConfig = { ...config, - url: config.url!, + url: url!, type: generateType(config), - method: config.method!.toUpperCase() as AxiosAdapterRequestMethod, + method: method!.toUpperCase() as AxiosAdapterRequestMethod, success, fail, }; - const adapterTask = config.adapter!(adapterConfig); + const adapterTask = adapter!(adapterConfig); function success(_: AxiosAdapterResponse): void { const response = _ as AxiosResponse; @@ -86,7 +83,6 @@ export function request(config: AxiosRequestConfig) { tryToggleProgressUpdate(adapterConfig, adapterTask.onProgressUpdate); } - const { cancelToken } = config; if (isCancelToken(cancelToken)) { cancelToken.onCancel((reason) => { if (isPlainObject(adapterTask)) { diff --git a/src/index.ts b/src/index.ts index c17f103..9aebb26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,13 @@ export type { AxiosRequestData, AxiosRequestFormData, AxiosResponse, + AxiosResponseData, AxiosResponseError, } from './core/Axios'; export type { AxiosAdapterRequestConfig, AxiosAdapterResponse, + AxiosAdapterResponseData, AxiosAdapterResponseError, AxiosAdapter, AxiosPlatform, @@ -19,4 +21,5 @@ export type { AxiosInstance, AxiosStatic, } from './axios'; + export default axios; diff --git a/test/core/dispatchRequest.test.ts b/test/core/dispatchRequest.test.ts index d81a067..13f86b2 100644 --- a/test/core/dispatchRequest.test.ts +++ b/test/core/dispatchRequest.test.ts @@ -1,8 +1,189 @@ -import { describe, test, expect } from 'vitest'; -import { dispatchRequest } from 'src/core/dispatchRequest'; +import { describe, test, expect, vi } from 'vitest'; +import { + asyncNext, + mockAdapter, + mockAdapterError, + mockAdapterFail, +} from 'scripts/test.utils'; +import { dispatchRequest } from '@/core/dispatchRequest'; +import _defaults from '@/defaults'; +import axios from '@/axios'; describe('src/core/dispatchRequest.ts', () => { - test('应该有这些实例属性', () => { - expect(dispatchRequest).toBeTypeOf('function'); + const defaults = { + ..._defaults, + adapter: mockAdapter(), + baseURL: 'http://api.com', + method: 'get' as const, + headers: {}, + }; + + test('应该抛出异常', () => { + expect(() => dispatchRequest({})).toThrowErrorMatchingInlineSnapshot( + '"[axios-miniprogram]: adapter 不是一个 function"', + ); + expect(() => + dispatchRequest({ adapter: mockAdapter() }), + ).toThrowErrorMatchingInlineSnapshot( + '"[axios-miniprogram]: url 不是一个 string"', + ); + expect(() => + dispatchRequest({ adapter: mockAdapter(), url: '/' }), + ).toThrowErrorMatchingInlineSnapshot( + '"[axios-miniprogram]: method 不是一个 string"', + ); + }); + + test('应该支持转换 URL', () => { + const c1 = { + ...defaults, + url: 'test', + }; + const c2 = { + ...defaults, + url: 'test/:id', + params: { + id: 1, + }, + }; + const c3 = { + ...defaults, + url: 'test/:id', + data: { + id: 1, + }, + }; + + dispatchRequest(c1); + dispatchRequest(c2); + dispatchRequest(c3); + + expect(c1.url).toBe('http://api.com/test'); + expect(c2.url).toBe('http://api.com/test/1?id=1'); + expect(c3.url).toBe('http://api.com/test/1'); + }); + + test('应该支持拉平请求头', () => { + const c = { + ...defaults, + url: 'test', + headers: { + common: { + h1: 1, + }, + get: { + h2: 2, + }, + h3: 3, + }, + }; + + dispatchRequest(c); + + expect(c.headers).toEqual({ + h1: 1, + h2: 2, + h3: 3, + }); + }); + + test('应该支持转换请求数据', () => { + const c = { + ...defaults, + url: 'test', + data: {}, + transformRequest: () => ({ id: 1 }), + }; + + dispatchRequest(c); + + expect(c.data).toEqual({ id: 1 }); + }); + + test('应该支持转换响应数据', async () => { + const c = { + ...defaults, + url: 'test', + transformResponse: () => ({ result: 1 }), + }; + + const r = await dispatchRequest(c); + + expect(r.data).toEqual({ result: 1 }); + }); + + test('应该支持自定义异常处理器', async () => { + const e1 = vi.fn(); + const e2 = vi.fn(); + const c1 = { + ...defaults, + adapter: mockAdapterError(), + url: 'test', + errorHandler: e1, + }; + const c2 = { + ...defaults, + adapter: mockAdapterFail(), + url: 'test', + errorHandler: e2, + }; + + try { + await dispatchRequest(c1); + } catch (err) { + expect(e1).toBeCalled(); + expect(e1.mock.calls[0][0]).toBe(err); + expect(axios.isAxiosError(err)).toBeTruthy(); + } + + try { + await dispatchRequest(c2); + } catch (err) { + expect(e2).toBeCalled(); + expect(e2.mock.calls[0][0]).toBe(err); + expect(axios.isAxiosError(err)).toBeTruthy(); + } + }); + + test('请求发送前取消请求应该抛出异常', async () => { + const cb = vi.fn(); + const { cancel, token } = axios.CancelToken.source(); + const c = { + ...defaults, + url: 'test', + cancelToken: token, + }; + + cancel(); + + try { + dispatchRequest(c); + } catch (err) { + cb(err); + } + + expect(cb).toBeCalled(); + expect(axios.isCancel(cb.mock.calls[0][0])).toBeTruthy(); + }); + + test('请求发送后取消请求应该抛出异常', async () => { + const cb = vi.fn(); + const { cancel, token } = axios.CancelToken.source(); + const c = { + ...defaults, + url: 'test', + cancelToken: token, + }; + + const p = dispatchRequest(c).catch(cb); + + await asyncNext(); + expect(cb).not.toBeCalled(); + + cancel(); + await p; + + expect(cb).toBeCalled(); + expect(axios.isCancel(cb.mock.calls[0][0])).toBeTruthy(); }); }); diff --git a/test/core/request.test.ts b/test/core/request.test.ts index e39e581..0406121 100644 --- a/test/core/request.test.ts +++ b/test/core/request.test.ts @@ -1,23 +1,14 @@ -import { describe, test, expect } from 'vitest'; +import { describe, test, expect, vi } from 'vitest'; import { mockAdapter, mockAdapterError, mockAdapterFail, } from 'scripts/test.utils'; import { request } from '@/core/request'; +import axios from '@/axios'; +import Axios from '@/core/Axios'; describe('src/core/request.ts', () => { - test('应该抛出异常', () => { - expect(request({})).rejects.toThrowErrorMatchingInlineSnapshot( - '"[axios-miniprogram]: adapter 不是一个 function"', - ); - expect( - request({ adapter: mockAdapter() }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - '"[axios-miniprogram]: url 不是一个 string"', - ); - }); - test('应该正确的响应请求', async () => { const s = request({ adapter: mockAdapter(), @@ -43,7 +34,9 @@ describe('src/core/request.ts', () => { "method": "get", "url": "/test", }, - "data": {}, + "data": { + "result": null, + }, "headers": {}, "request": { "abort": [Function], @@ -75,7 +68,9 @@ describe('src/core/request.ts', () => { "url": "/test", "validateStatus": [Function], }, - "data": {}, + "data": { + "result": null, + }, "headers": {}, "request": { "abort": [Function], @@ -104,7 +99,9 @@ describe('src/core/request.ts', () => { "method": "get", "url": "/test", }, - "data": {}, + "data": { + "result": null, + }, "headers": {}, "isFail": true, "request": { @@ -116,4 +113,75 @@ describe('src/core/request.ts', () => { } `); }); + + test('应该支持请求发送前取消请求', async () => { + const cb = vi.fn(); + const task = { + abort: vi.fn(), + }; + const { cancel, token } = axios.CancelToken.source(); + + cancel(); + + await request({ + adapter: () => task, + url: '/test', + method: 'get' as const, + cancelToken: token, + }).catch(cb); + + expect(task.abort).toBeCalled(); + expect(cb).toBeCalled(); + }); + + test('应该支持请求发送后取消请求', async () => { + const cb = vi.fn(); + const task = { + abort: vi.fn(), + }; + const { cancel, token } = axios.CancelToken.source(); + + const p = request({ + adapter: () => task, + url: '/test', + method: 'get' as const, + cancelToken: token, + }).catch(cb); + + cancel(); + await p; + + expect(task.abort).toBeCalled(); + expect(cb).toBeCalled(); + }); + + test('应该发送不同类型的请求', () => { + request({ + adapter: ({ type }) => { + expect(type).toBe('upload'); + }, + url: 'test', + method: 'post', + upload: true, + }); + + request({ + adapter: ({ type }) => { + expect(type).toBe('download'); + }, + url: 'test', + method: 'get', + download: true, + }); + + [...Axios.as, ...Axios.asp, ...Axios.asd].forEach((a) => { + request({ + adapter: ({ type }) => { + expect(type).toBe('request'); + }, + url: 'test', + method: a, + }); + }); + }); });