test: 测试 dispatchRequest

pull/41/head
zjx0905 2023-04-09 21:01:43 +08:00
parent 4c75fa3d32
commit ee588f6941
8 changed files with 320 additions and 57 deletions

View File

@ -50,11 +50,19 @@ export interface MockAdapterOptions {
after?: () => void; after?: () => void;
} }
export function mockAdapterBase( function mockAdapterBase(
type: 'success' | 'error' | 'fail' = 'success', type: 'success' | 'error' | 'fail' = 'success',
options: MockAdapterOptions = {}, options: MockAdapterOptions = {},
) { ) {
const { headers = {}, data = {}, delay = 0, before, after } = options; const {
headers = {},
data = {
result: null,
},
delay = 0,
before,
after,
} = options;
return (config: AxiosAdapterRequestConfig) => { return (config: AxiosAdapterRequestConfig) => {
let canceled = false; let canceled = false;

View File

@ -23,6 +23,8 @@ export type AxiosAdapterRequestMethod =
| 'TRACE' | 'TRACE'
| 'CONNECT'; | 'CONNECT';
export type AxiosAdapterResponseData = string | ArrayBuffer | AnyObject;
export interface AxiosAdapterResponse extends 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 { export interface AxiosAdapterResponseError extends AnyObject {
@ -139,17 +141,20 @@ export interface AxiosPlatform {
download: AxiosAdapterDownload; download: AxiosAdapterDownload;
} }
export type AxiosAdapterTask = { export type AxiosAdapterTask =
abort?(): void; | undefined
onProgressUpdate?(callback: AxiosProgressCallback): void; | void
offProgressUpdate?(callback: AxiosProgressCallback): void; | {
} | void; abort?(): void;
onProgressUpdate?(callback: AxiosProgressCallback): void;
offProgressUpdate?(callback: AxiosProgressCallback): void;
};
export interface AxiosAdapter { export interface AxiosAdapter {
(config: AxiosAdapterRequestConfig): AxiosAdapterTask; (config: AxiosAdapterRequestConfig): AxiosAdapterTask;
} }
export function getAdapterDefault(): AxiosAdapter | undefined { export function getAdapterDefault() {
const tryGetPlatforms = [ const tryGetPlatforms = [
() => uni, () => uni,
() => wx, () => wx,
@ -182,7 +187,7 @@ export function getAdapterDefault(): AxiosAdapter | undefined {
return createAdapter(platform); return createAdapter(platform);
} }
export function createAdapter(platform: AxiosPlatform): AxiosAdapter { export function createAdapter(platform: AxiosPlatform) {
assert(isPlainObject(platform), 'platform 不是一个 object'); assert(isPlainObject(platform), 'platform 不是一个 object');
assert(isFunction(platform.request), 'request 不是一个 function'); assert(isFunction(platform.request), 'request 不是一个 function');
assert(isFunction(platform.upload), 'upload 不是一个 function'); assert(isFunction(platform.upload), 'upload 不是一个 function');

View File

@ -1,7 +1,6 @@
import { buildURL } from '../helpers/buildURL'; import { buildURL } from '../helpers/buildURL';
import { isAbsoluteURL } from '../helpers/isAbsoluteURL'; import { isAbsoluteURL } from '../helpers/isAbsoluteURL';
import { combineURL } from '../helpers/combineURL'; import { combineURL } from '../helpers/combineURL';
import { mergeConfig } from './mergeConfig';
import { import {
AxiosAdapter, AxiosAdapter,
AxiosAdapterRequestMethod, AxiosAdapterRequestMethod,
@ -9,12 +8,14 @@ import {
AxiosAdapterResponse, AxiosAdapterResponse,
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,
AxiosAdapterResponseError, AxiosAdapterResponseError,
AxiosAdapterResponseData,
} from '../adapter'; } from '../adapter';
import { mergeConfig } from './mergeConfig';
import { CancelToken } from './cancel'; import { CancelToken } from './cancel';
import { dispatchRequest } from './dispatchRequest'; import { dispatchRequest } from './dispatchRequest';
import InterceptorManager from './InterceptorManager';
import { AxiosTransformer } from './transformData'; import { AxiosTransformer } from './transformData';
import AxiosDomain from './AxiosDomain'; import AxiosDomain from './AxiosDomain';
import InterceptorManager from './InterceptorManager';
export type AxiosRequestMethod = export type AxiosRequestMethod =
| AxiosAdapterRequestMethod | AxiosAdapterRequestMethod
@ -79,12 +80,7 @@ export interface AxiosRequestFormData extends AnyObject {
export type AxiosRequestData = AnyObject | AxiosRequestFormData; export type AxiosRequestData = AnyObject | AxiosRequestFormData;
export type AxiosResponseData = export type AxiosResponseData = undefined | number | AxiosAdapterResponseData;
| undefined
| number
| string
| ArrayBuffer
| AnyObject;
export interface AxiosProgressEvent { export interface AxiosProgressEvent {
progress: number; progress: number;
@ -151,7 +147,7 @@ export interface AxiosRequestConfig
/** /**
* *
*/ */
errorHandler?: (error: unknown) => Promise<AxiosResponse>; errorHandler?: (error: unknown) => Promise<void> | void;
/** /**
* *
*/ */
@ -241,10 +237,8 @@ export default class Axios extends AxiosDomain {
if (!isAbsoluteURL(baseURL)) { if (!isAbsoluteURL(baseURL)) {
defaults.baseURL = combineURL(this.defaults.baseURL ?? '', baseURL); defaults.baseURL = combineURL(this.defaults.baseURL ?? '', baseURL);
} }
return new AxiosDomain( return new AxiosDomain(mergeConfig(this.defaults, defaults), (config) =>
mergeConfig(this.defaults, defaults), this.#processRequest(config),
(config) => this.#processRequest(config),
); );
} }

View File

@ -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 { isCancel, isCancelToken } from './cancel';
import { flattenHeaders } from './flattenHeaders'; import { flattenHeaders } from './flattenHeaders';
import { AxiosTransformer, transformData } from './transformData'; import { AxiosTransformer, transformData } from './transformData';
@ -17,17 +18,19 @@ function throwIfCancellationRequested(config: AxiosRequestConfig) {
export function dispatchRequest(config: AxiosRequestConfig) { export function dispatchRequest(config: AxiosRequestConfig) {
throwIfCancellationRequested(config); 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.url = transformURL(config);
config.method = config.method ?? 'get';
config.headers = flattenHeaders(config); config.headers = flattenHeaders(config);
transformer(config, transformRequest); transformer(config, transformRequest);
function onSuccess(response: AxiosResponse) { function onSuccess(response: AxiosResponse) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
transformer(response, transformResponse); transformer(response, transformResponse);
return response; return response;
} }
@ -41,8 +44,13 @@ export function dispatchRequest(config: AxiosRequestConfig) {
} }
} }
if (isFunction(config.errorHandler)) { if (isFunction(errorHandler)) {
return config.errorHandler(reason); const promise = errorHandler(reason);
if (promise) {
return promise.then(() => {
throw reason;
});
}
} }
return Promise.reject(reason); return Promise.reject(reason);

View File

@ -1,5 +1,4 @@
import { isFunction, isPlainObject, isString } from '../helpers/isTypes'; import { isFunction, isPlainObject } from '../helpers/isTypes';
import { assert } from '../helpers/error';
import { import {
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,
AxiosAdapterRequestMethod, AxiosAdapterRequestMethod,
@ -10,7 +9,6 @@ import {
AxiosProgressCallback, AxiosProgressCallback,
AxiosRequestConfig, AxiosRequestConfig,
AxiosResponse, AxiosResponse,
AxiosResponseData,
AxiosResponseError, AxiosResponseError,
} from './Axios'; } from './Axios';
import { isCancelToken } from './cancel'; import { isCancelToken } from './cancel';
@ -41,19 +39,18 @@ function tryToggleProgressUpdate(
export function request(config: AxiosRequestConfig) { export function request(config: AxiosRequestConfig) {
return new Promise<AxiosResponse>((resolve, reject) => { return new Promise<AxiosResponse>((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 不是一个 function'); const { adapter, url, method, cancelToken } = config;
assert(isString(config.url), 'url 不是一个 string');
const adapterConfig: AxiosAdapterRequestConfig = { const adapterConfig: AxiosAdapterRequestConfig = {
...config, ...config,
url: config.url!, url: url!,
type: generateType(config), type: generateType(config),
method: config.method!.toUpperCase() as AxiosAdapterRequestMethod, method: method!.toUpperCase() as AxiosAdapterRequestMethod,
success, success,
fail, fail,
}; };
const adapterTask = config.adapter!(adapterConfig); const adapterTask = adapter!(adapterConfig);
function success(_: AxiosAdapterResponse): void { function success(_: AxiosAdapterResponse): void {
const response = _ as AxiosResponse; const response = _ as AxiosResponse;
@ -86,7 +83,6 @@ export function request(config: AxiosRequestConfig) {
tryToggleProgressUpdate(adapterConfig, adapterTask.onProgressUpdate); tryToggleProgressUpdate(adapterConfig, adapterTask.onProgressUpdate);
} }
const { cancelToken } = config;
if (isCancelToken(cancelToken)) { if (isCancelToken(cancelToken)) {
cancelToken.onCancel((reason) => { cancelToken.onCancel((reason) => {
if (isPlainObject(adapterTask)) { if (isPlainObject(adapterTask)) {

View File

@ -5,11 +5,13 @@ export type {
AxiosRequestData, AxiosRequestData,
AxiosRequestFormData, AxiosRequestFormData,
AxiosResponse, AxiosResponse,
AxiosResponseData,
AxiosResponseError, AxiosResponseError,
} from './core/Axios'; } from './core/Axios';
export type { export type {
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,
AxiosAdapterResponse, AxiosAdapterResponse,
AxiosAdapterResponseData,
AxiosAdapterResponseError, AxiosAdapterResponseError,
AxiosAdapter, AxiosAdapter,
AxiosPlatform, AxiosPlatform,
@ -19,4 +21,5 @@ export type {
AxiosInstance, AxiosInstance,
AxiosStatic, AxiosStatic,
} from './axios'; } from './axios';
export default axios; export default axios;

View File

@ -1,8 +1,189 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import { dispatchRequest } from 'src/core/dispatchRequest'; 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', () => { describe('src/core/dispatchRequest.ts', () => {
test('应该有这些实例属性', () => { const defaults = {
expect(dispatchRequest).toBeTypeOf('function'); ..._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();
}); });
}); });

View File

@ -1,23 +1,14 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import { import {
mockAdapter, mockAdapter,
mockAdapterError, mockAdapterError,
mockAdapterFail, mockAdapterFail,
} from 'scripts/test.utils'; } from 'scripts/test.utils';
import { request } from '@/core/request'; import { request } from '@/core/request';
import axios from '@/axios';
import Axios from '@/core/Axios';
describe('src/core/request.ts', () => { 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 () => { test('应该正确的响应请求', async () => {
const s = request({ const s = request({
adapter: mockAdapter(), adapter: mockAdapter(),
@ -43,7 +34,9 @@ describe('src/core/request.ts', () => {
"method": "get", "method": "get",
"url": "/test", "url": "/test",
}, },
"data": {}, "data": {
"result": null,
},
"headers": {}, "headers": {},
"request": { "request": {
"abort": [Function], "abort": [Function],
@ -75,7 +68,9 @@ describe('src/core/request.ts', () => {
"url": "/test", "url": "/test",
"validateStatus": [Function], "validateStatus": [Function],
}, },
"data": {}, "data": {
"result": null,
},
"headers": {}, "headers": {},
"request": { "request": {
"abort": [Function], "abort": [Function],
@ -104,7 +99,9 @@ describe('src/core/request.ts', () => {
"method": "get", "method": "get",
"url": "/test", "url": "/test",
}, },
"data": {}, "data": {
"result": null,
},
"headers": {}, "headers": {},
"isFail": true, "isFail": true,
"request": { "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,
});
});
});
}); });