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;
}
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;

View File

@ -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');

View File

@ -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<AxiosResponse>;
errorHandler?: (error: unknown) => Promise<void> | 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),
);
}

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 { 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);

View File

@ -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<AxiosResponse>((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)) {

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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,
});
});
});
});