feat: 支持合并自定义配置

#38
pull/41/head
zjx0905 2023-04-03 21:03:33 +08:00
parent ce12a83873
commit 4409a5720b
27 changed files with 821 additions and 376 deletions

View File

@ -13,6 +13,8 @@ axios.defaults.adapter = function adapter(adapterConfig) {
url, url,
// 请求方法 // 请求方法
method, method,
// 请求参数
params,
// 请求数据 // 请求数据
data, data,
// 请求头 同 headers // 请求头 同 headers
@ -53,7 +55,7 @@ axios.defaults.adapter = function adapter(adapterConfig) {
return wx.downloadFile({ return wx.downloadFile({
url, url,
method, method,
filePath: data.filePath, filePath: params.filePath,
header: headers, header: headers,
success, success,
fail, fail,

11
global.d.ts vendored
View File

@ -1,12 +1 @@
declare const uni: unknown;
declare const wx: unknown;
declare const my: unknown;
declare const swan: unknown;
declare const tt: unknown;
declare const qq: unknown;
declare const qh: unknown;
declare const ks: unknown;
declare const dd: unknown;
declare const jd: unknown;
type AnyObject<T = any> = Record<string, T>; type AnyObject<T = any> = Record<string, T>;

10
global.variables.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare const uni: unknown;
declare const wx: unknown;
declare const my: unknown;
declare const swan: unknown;
declare const tt: unknown;
declare const qq: unknown;
declare const qh: unknown;
declare const ks: unknown;
declare const dd: unknown;
declare const jd: unknown;

View File

@ -23,7 +23,7 @@ function main() {
function buildConfig(format) { function buildConfig(format) {
const isDts = format === 'dts'; const isDts = format === 'dts';
const output = { const output = {
file: resolvePath(format, isDts), file: resolveOutput(format, isDts),
format: isDts ? 'es' : format, format: isDts ? 'es' : format,
name: pkg.name, name: pkg.name,
exports: 'default', exports: 'default',
@ -51,7 +51,7 @@ function buildConfig(format) {
}; };
} }
function resolvePath(format, isDts) { function resolveOutput(format, isDts) {
return path.resolve( return path.resolve(
distPath, distPath,
`${pkg.name}${isDts ? '.d.ts' : `.${format}.js`}`, `${pkg.name}${isDts ? '.d.ts' : `.${format}.js`}`,

View File

@ -8,7 +8,7 @@ export function asyncTimeout(delay = 0) {
return new Promise((resolve) => setTimeout(resolve, delay)); return new Promise((resolve) => setTimeout(resolve, delay));
} }
export function captureError<T = any>(fn: () => void): T { export function captureError<T = any>(fn: () => void) {
try { try {
fn(); fn();
throw new Error('without Error'); throw new Error('without Error');
@ -17,7 +17,7 @@ export function captureError<T = any>(fn: () => void): T {
} }
} }
export function cleanedStack(error: Error) { export function checkStack(error: Error) {
if (error.stack) { if (error.stack) {
return error.stack.indexOf('at') === error.stack.indexOf('at /'); return error.stack.indexOf('at') === error.stack.indexOf('at /');
} }
@ -28,7 +28,7 @@ export function noop() {
return; return;
} }
export function mockResponse( export function mockResponseBase(
status: number, status: number,
statusText: string, statusText: string,
headers: AnyObject, headers: AnyObject,
@ -42,12 +42,15 @@ export function mockResponse(
}; };
} }
export function mockSuccess(headers: AnyObject = {}, data: AnyObject = {}) { export function mockResponse(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponse(200, 'OK', headers, data); return mockResponseBase(200, 'OK', headers, data);
} }
export function mockFail(headers: AnyObject = {}, data: AnyObject = {}) { export function mockResponseError(
return mockResponse(400, 'FAIL', headers, data); headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponseBase(400, 'FAIL', headers, data);
} }
export interface MockAdapterOptions { export interface MockAdapterOptions {
@ -58,8 +61,8 @@ export interface MockAdapterOptions {
after?: () => void; after?: () => void;
} }
export function mockAdapter( export function mockAdapterBase(
type: 'success' | 'fail', type: 'success' | 'error' | 'fail' = 'success',
options: MockAdapterOptions = {}, options: MockAdapterOptions = {},
) { ) {
const { headers = {}, data = {}, delay = 0, before, after } = options; const { headers = {}, data = {}, delay = 0, before, after } = options;
@ -67,20 +70,30 @@ export function mockAdapter(
return (config: AxiosAdapterRequestConfig) => { return (config: AxiosAdapterRequestConfig) => {
before?.(config); before?.(config);
setTimeout(() => { setTimeout(() => {
if (type === 'success') { switch (type) {
config.success(mockSuccess(headers, data)); case 'success':
} else { config.success(mockResponse(headers, data));
config.fail(mockFail(headers, data)); break;
case 'error':
config.success(mockResponseError(headers, data));
break;
case 'fail':
config.fail(mockResponseError(headers));
break;
} }
after?.(); after?.();
}, delay); }, delay);
}; };
} }
export function mockAdapterSuccess(options: MockAdapterOptions = {}) { export function mockAdapter(options: MockAdapterOptions = {}) {
return mockAdapter('success', options); return mockAdapterBase('success', options);
}
export function mockAdapterError(options: MockAdapterOptions = {}) {
return mockAdapterBase('error', options);
} }
export function mockAdapterFail(options: MockAdapterOptions = {}) { export function mockAdapterFail(options: MockAdapterOptions = {}) {
return mockAdapter('fail', options); return mockAdapterBase('fail', options);
} }

View File

@ -198,20 +198,34 @@ export default class Axios {
this.defaults = defaults; this.defaults = defaults;
for (const alias of Axios.as) { for (const alias of Axios.as) {
this[alias] = (url, config) => { this[alias] = (url, config = {}) => {
return this._req(alias, url, undefined, config); return this.request({
...config,
method: alias,
url,
});
}; };
} }
for (const alias of Axios.pas) { for (const alias of Axios.pas) {
this[alias] = (url, params, config) => { this[alias] = (url, params, config = {}) => {
return this._req(alias, url, params, config); return this.request({
...config,
method: alias,
params,
url,
});
}; };
} }
for (const alias of Axios.das) { for (const alias of Axios.das) {
this[alias] = (url, data, config) => { this[alias] = (url, data, config) => {
return this._reqWithData(alias, url, data, config); return this.request({
...config,
method: alias,
data,
url,
});
}; };
} }
} }
@ -249,32 +263,4 @@ export default class Axios {
return promiseResponse as Promise<AxiosResponse<TData>>; return promiseResponse as Promise<AxiosResponse<TData>>;
} }
private _req<TData = unknown>(
method: AxiosRequestMethod,
url: string,
params?: AnyObject,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this.request<TData>({
...(config ?? {}),
method,
url,
params,
});
}
private _reqWithData<TData = unknown>(
method: AxiosRequestMethod,
url: string,
data?: AnyObject | AxiosRequestFormData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this.request<TData>({
...(config ?? {}),
method,
url,
data,
});
}
} }

View File

@ -27,32 +27,28 @@ export default function dispatchRequest<TData = unknown>(
config.transformRequest, config.transformRequest,
); );
return request<TData>(config).then( function transformer(response: AxiosResponse<TData>) {
(response: AxiosResponse<TData>) => { response.data = transformData(
response.data as AnyObject,
response.headers,
config.transformResponse,
) as TData;
}
return request<TData>(config)
.then((response: AxiosResponse<TData>) => {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
transformer(response);
response.data = transformData(
response.data as AnyObject,
response.headers,
config.transformResponse,
) as TData;
return response; return response;
}, })
(reason: unknown) => { .catch((reason: unknown) => {
if (!isCancel(reason)) { if (!isCancel(reason)) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
const response = (reason as AnyObject)?.response;
if (isPlainObject(reason) && isPlainObject(reason.response)) { if (isPlainObject(response)) {
reason.response.data = transformData( transformer(response as AxiosResponse<TData>);
reason.response.data,
reason.response.headers,
config.transformResponse,
);
} }
} }
throw config.errorHandler?.(reason) ?? reason; throw config.errorHandler?.(reason) ?? reason;
}, });
);
} }

View File

@ -2,35 +2,17 @@ import { isUndefined, isPlainObject } from '../helpers/isTypes';
import { deepMerge } from '../helpers/deepMerge'; import { deepMerge } from '../helpers/deepMerge';
import { AxiosRequestConfig } from './Axios'; import { AxiosRequestConfig } from './Axios';
type AxiosRequestConfigKey = keyof AxiosRequestConfig; const fromConfig2Map: Record<string, boolean> = {
url: true,
const onlyFromConfig2Keys: AxiosRequestConfigKey[] = [ method: true,
'url', data: true,
'method', upload: true,
'data', download: true,
'upload', };
'download', const deepMergeConfigMap: Record<string, boolean> = {
]; headers: true,
const priorityFromConfig2Keys: AxiosRequestConfigKey[] = [ params: true,
'adapter', };
'baseURL',
'paramsSerializer',
'transformRequest',
'transformResponse',
'errorHandler',
'cancelToken',
'dataType',
'responseType',
'timeout',
'enableHttp2',
'enableQuic',
'enableCache',
'sslVerify',
'validateStatus',
'onUploadProgress',
'onDownloadProgress',
];
const deepMergeConfigKeys: AxiosRequestConfigKey[] = ['headers', 'params'];
export function mergeConfig( export function mergeConfig(
config1: AxiosRequestConfig = {}, config1: AxiosRequestConfig = {},
@ -38,33 +20,36 @@ export function mergeConfig(
): AxiosRequestConfig { ): AxiosRequestConfig {
const config: AxiosRequestConfig = {}; const config: AxiosRequestConfig = {};
for (const key of onlyFromConfig2Keys) { // 所有已知键名
const value = config2[key]; const keysSet = Array.from(
new Set([...Object.keys(config1), ...Object.keys(config2)]),
);
if (!isUndefined(value)) { for (const key of keysSet) {
config[key] = value as any; const val1 = config1[key] as any;
const val2 = config2[key] as any;
// 只从 config2 中取值
if (fromConfig2Map[key]) {
if (!isUndefined(val2)) config[key] = val2;
} }
} // 深度合并 config1 和 config2 中的对象
else if (deepMergeConfigMap[key]) {
for (const key of priorityFromConfig2Keys) { if (isPlainObject(val1) && isPlainObject(val2)) {
const value1 = config1[key]; config[key] = deepMerge(val1, val2);
const value2 = config2[key]; } else if (isPlainObject(val1)) {
config[key] = deepMerge(val1);
if (!isUndefined(value2)) { } else if (isPlainObject(val2)) {
config[key] = value2 as any; config[key] = deepMerge(val2);
} else if (!isUndefined(value1)) { }
config[key] = value1 as any;
} }
} // 优先从 config2 中取值,如果没有值就从 config1 中取值
else {
for (const key of deepMergeConfigKeys) { if (!isUndefined(val2)) {
const value1 = config1[key]; config[key] = val2;
const value2 = config2[key]; } else if (!isUndefined(val1)) {
config[key] = val1;
if (isPlainObject(value2)) { }
config[key] = deepMerge(value1 ?? {}, value2) as any;
} else if (isPlainObject(value1)) {
config[key] = deepMerge(value1) as any;
} }
} }

View File

@ -1,4 +1,4 @@
import { isFunction, isPlainObject } from '../helpers/isTypes'; import { isFunction, isPlainObject, isString } from '../helpers/isTypes';
import { assert } from '../helpers/error'; import { assert } from '../helpers/error';
import { import {
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,
@ -42,10 +42,12 @@ export function request<TData = unknown>(
): Promise<AxiosResponse<TData>> { ): Promise<AxiosResponse<TData>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 不是一个 function'); assert(isFunction(config.adapter), 'adapter 不是一个 function');
assert(isString(config.url), 'url 不是一个 string');
assert(isString(config.method), 'method 不是一个 string');
const adapterConfig: AxiosAdapterRequestConfig = { const adapterConfig: AxiosAdapterRequestConfig = {
...config, ...config,
url: config.url ?? '', url: config.url,
type: generateType(config), type: generateType(config),
method: config.method!.toUpperCase() as AxiosAdapterRequestMethod, method: config.method!.toUpperCase() as AxiosAdapterRequestMethod,
success, success,

View File

@ -11,12 +11,13 @@ export function throwError(msg: string): void {
} }
export function cleanStack(error: Error) { export function cleanStack(error: Error) {
if (error.stack) { const { stack } = error;
const start = error.stack.indexOf('at'); if (stack) {
const end = error.stack.search(/at ([\w-_.]+:)?\//i); const start = stack.indexOf('at');
const end = stack.search(/at ([\w-_.]+:)?\//i);
if (start < end) { if (start < end) {
const removed = error.stack.slice(start, end); const removed = stack.slice(start, end);
error.stack = error.stack.replace(removed, ''); error.stack = stack.replace(removed, '');
} }
} }
} }

View File

@ -1,42 +1,42 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import axios from 'src/axios'; import axios from 'src/axios';
import { mockAdapterFail, mockAdapterSuccess } from 'scripts/test.utils'; import { mockAdapter, mockAdapterError } from 'scripts/test.utils';
describe('src/axios.ts', () => { describe('src/axios.ts', () => {
test('应该处理成功和失败', () => { test('应该处理成功和失败', () => {
axios({ axios({
adapter: mockAdapterSuccess({ adapter: mockAdapter({
headers: { type: 'json' }, headers: { type: 'json' },
data: { v1: 1 }, data: { v1: 1 },
before: (config) => { before: (config) => {
expect(config.url).toBe('http://api.com/user/1?id=1'); expect(config.url).toBe('http://api.com/test/1?id=1');
}, },
}), }),
baseURL: 'http://api.com', baseURL: 'http://api.com',
url: 'user/:id', url: 'test/:id',
params: { params: {
id: 1, id: 1,
}, },
}).then((response) => { }).then((res) => {
expect(response.headers).toEqual({ type: 'json' }); expect(res.headers).toEqual({ type: 'json' });
expect(response.data).toEqual({ v1: 1 }); expect(res.data).toEqual({ v1: 1 });
}); });
axios('user/:id', { axios('test/:id', {
adapter: mockAdapterFail({ adapter: mockAdapterError({
headers: { type: 'json' }, headers: { type: 'json' },
data: { v1: 1 }, data: { v1: 1 },
before: (config) => { before: (config) => {
expect(config.url).toBe('http://api.com/user/1'); expect(config.url).toBe('http://api.com/test/1');
}, },
}), }),
baseURL: 'http://api.com', baseURL: 'http://api.com',
data: { data: {
id: 1, id: 1,
}, },
}).catch((error) => { }).catch((err) => {
expect(error.response.headers).toEqual({ type: 'json' }); expect(err.response.headers).toEqual({ type: 'json' });
expect(error.response.data).toEqual({ v1: 1 }); expect(err.response.data).toEqual({ v1: 1 });
}); });
}); });
}); });

View File

@ -2,7 +2,7 @@ import { describe, test, expect, vi } from 'vitest';
import { import {
asyncNext, asyncNext,
captureError, captureError,
mockAdapterSuccess, mockAdapter,
noop, noop,
asyncTimeout, asyncTimeout,
} from 'scripts/test.utils'; } from 'scripts/test.utils';
@ -16,17 +16,17 @@ import {
describe('src/helpers/cancel.ts', () => { describe('src/helpers/cancel.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {
const cancel = new Cancel(); const c = new Cancel();
expect(cancel.message).toBeUndefined(); expect(c.message).toBeUndefined();
expect(cancel.toString()).toBe('Cancel'); expect(c.toString()).toBe('Cancel');
}); });
test('传入参数时应该有正确的返回结果', () => { test('传入参数时应该有正确的返回结果', () => {
const cancel = new Cancel('error'); const c = new Cancel('error');
expect(cancel.message).toBe('error'); expect(c.message).toBe('error');
expect(cancel.toString()).toBe('Cancel: error'); expect(c.toString()).toBe('Cancel: error');
}); });
test('应该正确判断 Cancel', () => { test('应该正确判断 Cancel', () => {
@ -36,44 +36,47 @@ describe('src/helpers/cancel.ts', () => {
}); });
test('应该可以取消', () => { test('应该可以取消', () => {
let cancelAction!: () => void; let ca!: () => void;
const cancelToken = new CancelToken((action) => { const ct = new CancelToken((a) => (ca = a));
cancelAction = action;
});
expect(cancelToken.throwIfRequested()).toBeUndefined(); expect(ct.throwIfRequested()).toBeUndefined();
cancelAction();
expect(() => cancelToken.throwIfRequested()).toThrowError(); ca();
expect(() => ct.throwIfRequested()).toThrowError();
}); });
test('应该抛出正确的异常信息', async () => { test('应该抛出正确的异常信息', async () => {
let cancelAction!: (msg: string) => void; let ca!: (msg: string) => void;
const cancelToken = new CancelToken((action) => { const ct = new CancelToken((a) => (ca = a));
cancelAction = action;
});
cancelAction('stop'); const te = () => ct.throwIfRequested();
const error = captureError<Cancel>(() => cancelToken.throwIfRequested());
expect(error.message).toBe('stop'); ca('stop');
expect(error.toString()).toBe('Cancel: stop');
expect(te).toThrowErrorMatchingInlineSnapshot(`
Cancel {
"message": "stop",
}
`);
expect(captureError<Cancel>(te).toString()).toBe('Cancel: stop');
}); });
test('回调函数应该被异步执行', async () => { test('回调函数应该被异步执行', async () => {
const canceled = vi.fn(); const cb = vi.fn();
let cancelAction!: () => void; let ca!: () => void;
const cancelToken = new CancelToken((action) => { const ct = new CancelToken((a) => (ca = a));
cancelAction = action;
});
cancelToken.onCancel(canceled);
expect(canceled).not.toBeCalled();
cancelAction(); ct.onCancel(cb);
expect(cb).not.toBeCalled();
expect(canceled).not.toBeCalled(); ca();
expect(cb).not.toBeCalled();
await asyncNext(); await asyncNext();
expect(canceled).toBeCalled(); expect(cb).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy(); expect(isCancel(cb.mock.calls[0][0])).toBeTruthy();
}); });
test('应该正确判断 CancelToken', () => { test('应该正确判断 CancelToken', () => {
@ -83,49 +86,51 @@ describe('src/helpers/cancel.ts', () => {
}); });
test('应该有正确返回结果', () => { test('应该有正确返回结果', () => {
const source = CancelToken.source(); const s = CancelToken.source();
expect(source.cancel).toBeTypeOf('function'); expect(s.cancel).toBeTypeOf('function');
expect(isCancelToken(source.token)).toBeTruthy(); expect(isCancelToken(s.token)).toBeTruthy();
}); });
test('应该可以取消', () => { test('应该可以取消', () => {
const source = CancelToken.source(); const s = CancelToken.source();
expect(source.token.throwIfRequested()).toBeUndefined(); expect(s.token.throwIfRequested()).toBeUndefined();
source.cancel(); s.cancel();
expect(() => source.token.throwIfRequested()).toThrowError(); expect(() => s.token.throwIfRequested()).toThrowError();
}); });
test('应该可以在请求发出之前取消', async () => { test('应该可以在请求发出之前取消', async () => {
const canceled = vi.fn(); const cb = vi.fn();
const source = CancelToken.source(); const s = CancelToken.source();
s.cancel();
source.cancel();
axios({ axios({
adapter: mockAdapterSuccess(), adapter: mockAdapter(),
cancelToken: source.token, cancelToken: s.token,
}).catch(canceled); }).catch(cb);
await asyncTimeout(); await asyncTimeout();
expect(canceled).toBeCalled(); expect(cb).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy(); expect(isCancel(cb.mock.calls[0][0])).toBeTruthy();
}); });
test('应该可以在请求发出之后取消', async () => { test('应该可以在请求发出之后取消', async () => {
const canceled = vi.fn(); const cb = vi.fn();
const source = CancelToken.source(); const s = CancelToken.source();
axios({ axios({
adapter: mockAdapterSuccess(), adapter: mockAdapter(),
cancelToken: source.token, cancelToken: s.token,
}).catch(canceled); }).catch(cb);
source.cancel();
s.cancel();
await asyncTimeout(); await asyncTimeout();
expect(canceled).toBeCalled(); expect(cb).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy(); expect(isCancel(cb.mock.calls[0][0])).toBeTruthy();
}); });
}); });

View File

@ -1,27 +1,27 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { cleanedStack } from 'scripts/test.utils'; import { checkStack } from 'scripts/test.utils';
import { createError } from 'src/core/createError'; import { createError } from 'src/core/createError';
describe('src/core/createError.ts', () => { describe('src/core/createError.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {
const config = {}; const c = {};
const axiosError = createError('error', config); const err = createError('error', c);
expect(axiosError.isAxiosError).toBeTruthy(); expect(err.isAxiosError).toBeTruthy();
expect(axiosError.message).toBe('error'); expect(err.message).toBe('error');
expect(axiosError.config).toBe(config); expect(err.config).toBe(c);
expect(cleanedStack(axiosError)).toBeTruthy(); expect(checkStack(err)).toBeTruthy();
}); });
test('应该支持传入更多参数', () => { test('应该支持传入更多参数', () => {
const config = {}; const c = {};
const request = {}; const req = {};
const response = {}; const res = {};
const axiosError = createError('error', config, request, response as any); const err = createError('error', c, req, res as any);
expect(axiosError.message).toBe('error'); expect(err.message).toBe('error');
expect(axiosError.config).toBe(config); expect(err.config).toBe(c);
expect(axiosError.request).toBe(request); expect(err.request).toBe(req);
expect(axiosError.response).toBe(response); expect(err.response).toBe(res);
}); });
}); });

View File

@ -0,0 +1,115 @@
import { describe, test, expect } from 'vitest';
import { flattenHeaders } from 'src/core/flattenHeaders';
import Axios from 'src/core/Axios';
describe('src/core/flattenHeaders.ts', () => {
const keys = [...Axios.as, ...Axios.pas, ...Axios.das];
const baseHeaders = {
options: {
v1: 'options1',
v2: 'options2',
},
trace: {
v1: 'trace1',
v2: 'trace2',
},
connect: {
v1: 'connect1',
v2: 'connect2',
},
head: {
v1: 'head1',
v2: 'head2',
},
get: {
v1: 'get1',
v2: 'get2',
},
delete: {
v1: 'delete1',
v2: 'delete2',
},
post: {
v1: 'post1',
v2: 'post2',
},
put: {
v1: 'put1',
v2: 'put2',
},
};
test('应该支持空配置', () => {
expect(flattenHeaders({})).toBeUndefined();
});
test('应该支持自定义 headers', () => {
const h = {
v1: '1',
v2: '2',
};
expect(flattenHeaders({ headers: h, method: 'get' })).toEqual(h);
});
test('应该支持别名 headers并且自定义 headers 优先级应该高于别名 headers', () => {
const h1 = baseHeaders;
const h2 = { v1: 1, v2: 2 };
const h3 = { ...h1, ...h2 };
keys.forEach((a) => {
expect(flattenHeaders({ headers: h1, method: a })).toEqual(h1[a]);
expect(flattenHeaders({ headers: h3, method: a })).toEqual(h2);
});
});
test('应该支持通用 headers并且别名 headers 优先级应该高于通用 headers', () => {
const h1 = {
common: {
v1: 'common1',
v2: 'common2',
},
};
const h2 = { ...baseHeaders, ...h1 };
keys.forEach((a) => {
expect(flattenHeaders({ headers: h1, method: a })).toEqual(h1.common);
expect(flattenHeaders({ headers: h2, method: a })).toEqual(h2[a]);
});
});
test.each(
keys.map((k) => [
k,
{
common: {
v1: 'common1',
v2: 'common1',
},
[k]: {
v3: `${k}1`,
v4: `${k}2`,
},
v5: 5,
v6: 6,
},
]),
)('应该获取到完整的 %s headers', (k, h) => {
const h1 = {
v1: 'common1',
v2: 'common1',
v5: 5,
v6: 6,
};
const h2 = {
...h1,
v3: `${k}1`,
v4: `${k}2`,
};
keys.forEach((a) => {
expect(flattenHeaders({ headers: h, method: a })).toEqual(
a !== k ? h1 : h2,
);
});
});
});

View File

@ -1,50 +1,19 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { mockAdapterSuccess } from 'scripts/test.utils';
import { generateType } from 'src/core/generateType'; import { generateType } from 'src/core/generateType';
import Axios from 'src/core/Axios'; import Axios from 'src/core/Axios';
import axios from 'src/axios';
describe('src/core/generateType.ts', () => { describe('src/core/generateType.ts', () => {
test('应该是一个 reuqest', () => { test('应该是一个 reuqest', () => {
for (const alias of [...Axios.as, ...Axios.pas, ...Axios.das]) { for (const a of [...Axios.as, ...Axios.pas, ...Axios.das]) {
expect(generateType({ method: alias })).toBe('request'); expect(generateType({ method: a })).toBe('request');
axios({
adapter: mockAdapterSuccess({
before: (config) => {
expect(config.type).toBe('request');
},
}),
method: alias,
});
} }
}); });
test('应该是一个 upload', () => { test('应该是一个 upload', () => {
expect(generateType({ method: 'post', upload: true })).toBe('upload'); expect(generateType({ method: 'post', upload: true })).toBe('upload');
axios({
adapter: mockAdapterSuccess({
before: (config) => {
expect(config.type).toBe('upload');
},
}),
method: 'post',
upload: true,
});
}); });
test('应该是一个 download', () => { test('应该是一个 download', () => {
expect(generateType({ method: 'get', download: true })).toBe('download'); expect(generateType({ method: 'get', download: true })).toBe('download');
axios({
adapter: mockAdapterSuccess({
before: (config) => {
expect(config.type).toBe('download');
},
}),
method: 'get',
download: true,
});
}); });
}); });

View File

@ -0,0 +1,173 @@
import { describe, test, expect, vi } from 'vitest';
import { ignore } from 'src/helpers/ignore';
import { mergeConfig } from 'src/core/mergeConfig';
import { CancelToken } from 'src/core/cancel';
describe('src/core/mergeConfig.ts', () => {
test('应该支持空参数', () => {
expect(mergeConfig()).toEqual({});
expect(mergeConfig({ baseURL: '/api' })).toEqual({ baseURL: '/api' });
});
test('应该只取 config2', () => {
const c1 = {
url: 'a',
method: 'get' as const,
data: {},
upload: true,
download: true,
};
const c2 = {
url: 'b',
method: 'post' as const,
data: {},
upload: false,
download: false,
};
expect(mergeConfig(c1, {})).toEqual({});
expect(mergeConfig({}, c2)).toEqual(c2);
expect(mergeConfig(c1, c2)).toEqual(c2);
Object.keys(c2).forEach((_) => {
const key = _ as keyof typeof c2;
expect(mergeConfig(ignore(c1, key), c2)).toEqual(c2);
expect(mergeConfig(c1, ignore(c2, key))).toEqual(ignore(c2, key));
});
});
test('应该深度合并', () => {
const c1 = {
headers: {
common: {
v1: 1,
},
v1: 1,
},
params: {
v1: 1,
},
};
const c2 = {
headers: {
common: {
v2: 2,
},
v2: 2,
},
params: {
v2: 2,
},
};
const mc = {
headers: {
common: {
v1: 1,
v2: 2,
},
v1: 1,
v2: 2,
},
params: {
v1: 1,
v2: 2,
},
};
expect(mergeConfig(c1, {})).toEqual(c1);
expect(mergeConfig({}, c2)).toEqual(c2);
expect(mergeConfig(c1, c2)).toEqual(mc);
Object.keys(c2).forEach((_) => {
const key = _ as keyof typeof c2;
expect(mergeConfig(ignore(c1, key), c2)).toEqual({
...mc,
[key]: c2[key],
});
expect(mergeConfig(c1, ignore(c2, key))).toEqual({
...mc,
[key]: c1[key],
});
});
});
test('应该优先取 config2', () => {
const c1 = {
adapter: vi.fn(),
baseURL: 'https://c1.com',
paramsSerializer: vi.fn(),
transformRequest: vi.fn(),
transformResponse: vi.fn(),
errorHandler: vi.fn(),
cancelToken: CancelToken.source().token,
dataType: 'json',
responseType: 'json',
timeout: 1000,
validateStatus: vi.fn(),
onUploadProgress: vi.fn(),
onDownloadProgress: vi.fn(),
};
const c2 = {
adapter: vi.fn(),
baseURL: 'https://c2.com',
paramsSerializer: vi.fn(),
transformRequest: vi.fn(),
transformResponse: vi.fn(),
errorHandler: vi.fn(),
cancelToken: CancelToken.source().token,
dataType: 'json',
responseType: 'json',
timeout: 1000,
validateStatus: vi.fn(),
onUploadProgress: vi.fn(),
onDownloadProgress: vi.fn(),
};
expect(mergeConfig(c1, {})).toEqual(c1);
expect(mergeConfig({}, c2)).toEqual(c2);
expect(mergeConfig(c1, c2)).toEqual(c2);
Object.keys(c2).forEach((_) => {
const key = _ as keyof typeof c2;
expect(mergeConfig(ignore(c1, key), c2)).toEqual(c2);
expect(mergeConfig(c1, ignore(c2, key))).toEqual({
...c2,
[key]: c1[key],
});
});
});
test('应该支持自定义配置', () => {
const c1 = {
custom1: 1,
custom2: 'c1',
custom3: vi.fn(),
custom4: { c1: 1 },
custom5: ['c1'],
custom6: new Date(),
custom7: () => 1,
};
const c2 = {
custom1: 2,
custom2: 'c2',
custom3: vi.fn(),
custom4: { c2: 2 },
custom5: ['c2'],
custom6: new Date(),
custom7: () => 2,
};
expect(mergeConfig(c1, {})).toEqual(c1);
expect(mergeConfig({}, c2)).toEqual(c2);
expect(mergeConfig(c1, c2)).toEqual(c2);
Object.keys(c2).forEach((_) => {
const key = _ as keyof typeof c2;
expect(mergeConfig(ignore(c1, key), c2)).toEqual(c2);
expect(mergeConfig(c1, ignore(c2, key))).toEqual({
...c2,
[key]: c1[key],
});
});
});
});

75
test/core/request.test.ts Normal file
View File

@ -0,0 +1,75 @@
import { describe, test, expect } from 'vitest';
import { request } from 'src/core/request';
import {
mockAdapter,
mockAdapterError,
mockAdapterFail,
} from 'scripts/test.utils';
describe('src/core/request.ts', () => {
test('应该抛出异常', async () => {
await expect(request({})).rejects.toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: adapter 不是一个 function"',
);
await expect(
request({ adapter: mockAdapter() }),
).rejects.toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: url 不是一个 string"',
);
await expect(
request({ adapter: mockAdapter(), url: 'test' }),
).rejects.toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: method 不是一个 string"',
);
});
test('应该能够取到数据', async () => {
await expect(
request({
adapter: mockAdapter(),
url: '/test',
method: 'get',
}),
).resolves.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
},
"data": {},
"headers": {},
"request": undefined,
"status": 200,
"statusText": "OK",
}
`);
await expect(
request({
adapter: mockAdapterError(),
url: '/test',
method: 'get',
}),
).resolves.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
},
"data": {},
"headers": {},
"request": undefined,
"status": 400,
"statusText": "FAIL",
}
`);
await expect(
request({
adapter: mockAdapterFail(),
url: '/test',
method: 'get',
}),
).rejects.toThrowErrorMatchingInlineSnapshot('"网络错误"');
});
});

View File

@ -0,0 +1,58 @@
import { describe, test, expect } from 'vitest';
import { transformData } from 'src/core/transformData';
describe('src/core/transformData.ts', () => {
test('应该支持空配置', () => {
expect(transformData()).toBeUndefined();
expect(transformData({})).toEqual({});
});
test('应该支持转换器', () => {
const h = {
type: 0,
};
const d = {
v1: 1,
};
const t = {
v2: 2,
};
const fn = (data: any, headers: any) => {
expect(data).toEqual(d);
expect(headers).toEqual(h);
return t;
};
expect(transformData(d, h, fn)).toEqual(t);
});
test('应该支持转换器数组', () => {
const h = {
type: 0,
};
const d = {
v1: 1,
};
const t1 = {
v2: 2,
};
const t2 = {
v3: 3,
};
const fn1 = (data: any, headers: any) => {
expect(data).toEqual(d);
expect(headers).toEqual(h);
return t1;
};
const fn2 = (data: any, headers: any) => {
expect(data).toEqual(t1);
expect(headers).toEqual(h);
return t2;
};
expect(transformData(d, h, [fn1, fn2])).toEqual(t2);
});
});

View File

@ -0,0 +1,74 @@
import { describe, test, expect } from 'vitest';
import { transformURL } from 'src/core/transformURL';
describe('src/core/transformURL.ts', () => {
test('应该支持空配置', () => {
expect(transformURL({})).toBe('');
expect(transformURL({ baseURL: 'http://api.com' })).toBe('http://api.com');
expect(transformURL({ url: 'test' })).toBe('/test');
});
test('应该合并 URL', () => {
expect(
transformURL({
baseURL: 'http://api.com',
url: 'test',
}),
).toBe('http://api.com/test');
expect(
transformURL({
baseURL: 'http://api.com',
url: '/test',
}),
).toBe('http://api.com/test');
});
test('应该支持绝对路径', () => {
expect(
transformURL({
baseURL: 'http://api.com',
url: 'http://api2.com',
}),
).toBe('http://api2.com');
});
test('应该支持动态 URL', () => {
expect(
transformURL({
baseURL: 'http://api.com',
url: 'test/:name/:type',
params: {
name: 'axios',
type: 0,
},
}),
).toBe('http://api.com/test/axios/0?name=axios&type=0');
expect(
transformURL({
baseURL: 'http://api.com',
url: 'test/:name/:type',
data: {
name: 'axios',
type: 0,
},
}),
).toBe('http://api.com/test/axios/0');
});
test('应该支持自定义参数系列化器', () => {
expect(
transformURL({
baseURL: 'http://api.com',
url: 'test',
paramsSerializer: () => 'type=0',
}),
).toBe('http://api.com/test?type=0');
expect(
transformURL({
baseURL: 'http://api.com',
url: 'test?name=axios',
paramsSerializer: () => 'type=0',
}),
).toBe('http://api.com/test?name=axios&type=0');
});
});

View File

@ -3,61 +3,61 @@ import { buildURL } from 'src/helpers/buildURL';
describe('src/helpers/buildURL.ts', () => { describe('src/helpers/buildURL.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {
expect(buildURL('/user')).toBe('/user'); expect(buildURL('/test')).toBe('/test');
}); });
test('应该清理哈希值', () => { test('应该清理哈希值', () => {
expect(buildURL('/user#hash')).toBe('/user'); expect(buildURL('/test#hash')).toBe('/test');
}); });
test('应该对参数进行系列化', () => { test('应该对参数进行系列化', () => {
expect( expect(
buildURL('/user#hash', { buildURL('/test#hash', {
v1: 1, v1: 1,
v2: undefined, v2: undefined,
v3: null, v3: null,
v4: '4', v4: '4',
v5: NaN, v5: NaN,
}), }),
).toBe('/user?v1=1&v4=4'); ).toBe('/test?v1=1&v4=4');
expect( expect(
buildURL('/user?v1=1', { buildURL('/test?v1=1', {
v2: 2, v2: 2,
}), }),
).toBe('/user?v1=1&v2=2'); ).toBe('/test?v1=1&v2=2');
}); });
test('应该对数组进行系列化', () => { test('应该对数组进行系列化', () => {
expect( expect(
buildURL('/user', { buildURL('/test', {
arr: [1, 2], arr: [1, 2],
}), }),
).toBe('/user?arr[]=1&arr[]=2'); ).toBe('/test?arr[]=1&arr[]=2');
}); });
test('应该对对象进行系列化', () => { test('应该对对象进行系列化', () => {
expect( expect(
buildURL('/user', { buildURL('/test', {
obj: { obj: {
k1: 1, k1: 1,
k2: 2, k2: 2,
}, },
}), }),
).toBe('/user?obj[k1]=1&obj[k2]=2'); ).toBe('/test?obj[k1]=1&obj[k2]=2');
}); });
test('应该对日期进行系列化', () => { test('应该对日期进行系列化', () => {
const date = new Date(); const d = new Date();
expect(buildURL('/user', { date })).toBe( expect(buildURL('/test', { date: d })).toBe(
`/user?date=${date.toISOString()}`, `/test?date=${d.toISOString()}`,
); );
}); });
test('应该支持自定义序列化器', () => { test('应该支持自定义序列化器', () => {
expect(buildURL('/user', {}, () => 'v1=1&v2=2')).toBe('/user?v1=1&v2=2'); expect(buildURL('/test', {}, () => 'v1=1&v2=2')).toBe('/test?v1=1&v2=2');
expect(buildURL('/user?v1=1', {}, () => 'v2=2&v3=3')).toBe( expect(buildURL('/test?v1=1', {}, () => 'v2=2&v3=3')).toBe(
'/user?v1=1&v2=2&v3=3', '/test?v1=1&v2=2&v3=3',
); );
}); });
}); });

View File

@ -9,17 +9,17 @@ describe('src/helpers/combineURL.ts', () => {
}); });
test('应该得到拼接后的结果', () => { test('应该得到拼接后的结果', () => {
expect(combineURL('http://api.com', '/user')).toBe('http://api.com/user'); expect(combineURL('http://api.com', '/test')).toBe('http://api.com/test');
expect(combineURL('file://api.com', '/user')).toBe('file://api.com/user'); expect(combineURL('file://api.com', '/test')).toBe('file://api.com/test');
expect(combineURL('unknow://api.com', '/user')).toBe( expect(combineURL('unknow://api.com', '/test')).toBe(
'unknow://api.com/user', 'unknow://api.com/test',
); );
}); });
test('应该清理多余的斜线', () => { test('应该清理多余的斜线', () => {
expect(combineURL('//api//', '//user//')).toBe('/api/user/'); expect(combineURL('//api//', '//test//')).toBe('/api/test/');
expect(combineURL('http://api.com//', '//user//')).toBe( expect(combineURL('http://api.com//', '//test//')).toBe(
'http://api.com/user/', 'http://api.com/test/',
); );
}); });
}); });

View File

@ -3,66 +3,50 @@ import { deepMerge } from 'src/helpers/deepMerge';
describe('src/helpers/deepMerge.ts', () => { describe('src/helpers/deepMerge.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {
expect(deepMerge()).toEqual({}); const o = {
});
test('应该直接返回第一个参数', () => {
expect(
deepMerge({
v1: 1,
v2: [1],
v3: { v: 'v3' },
v4: undefined,
v5: null,
v6: 'v6',
}),
).toEqual({
v1: 1, v1: 1,
v2: [1], v2: [1],
v3: { v: 'v3' }, v3: { v: 'v3' },
v4: undefined, v4: undefined,
v5: null, v5: null,
v6: 'v6', v6: 'v6',
}); };
expect(deepMerge()).toEqual({});
expect(deepMerge(o)).toEqual(o);
}); });
test('应该进行合并', () => { test('应该进行合并', () => {
expect( const o1 = {
deepMerge(
{
v1: 1,
v2: 2,
v3: 3,
},
{
v2: 22,
v3: undefined,
v4: 4,
},
),
).toEqual({
v1: 1, v1: 1,
v2: 2,
v3: 3,
};
const o2 = {
v2: 22, v2: 22,
v3: undefined, v3: undefined,
v4: 4, v4: 4,
};
expect(deepMerge<AnyObject>(o1, o2)).toEqual({
...o1,
...o2,
}); });
}); });
test('应该合并对象里的对象', () => { test('应该合并对象里的对象', () => {
expect( const o1 = {
deepMerge( v1: { v: 1 },
{ v2: { v: 2 },
v1: { v: 1 }, v3: 3,
v2: { v: 2 }, };
v3: 3, const o2 = {
}, v1: { vv: 11 },
{ v2: 2,
v1: { vv: 11 }, v3: { v: 3 },
v2: 2, };
v3: { v: 3 },
}, expect(deepMerge<AnyObject>(o1, o2)).toEqual({
),
).toEqual({
v1: { v: 1, vv: 11 }, v1: { v: 1, vv: 11 },
v2: 2, v2: 2,
v3: { v: 3 }, v3: { v: 3 },

View File

@ -3,20 +3,20 @@ import { dynamicURL } from 'src/helpers/dynamicURL';
describe('src/helpers/dynamicURL.ts', () => { describe('src/helpers/dynamicURL.ts', () => {
test('应该替换关键字', () => { test('应该替换关键字', () => {
expect(dynamicURL('http://api.com/user/:id', {})).toBe( expect(dynamicURL('http://api.com/test/:id', {})).toBe(
'http://api.com/user/undefined', 'http://api.com/test/undefined',
); );
expect(dynamicURL('http://api.com/user/:id', { id: 1 })).toBe( expect(dynamicURL('http://api.com/test/:id', { id: 1 })).toBe(
'http://api.com/user/1', 'http://api.com/test/1',
); );
}); });
test('应该支持多个关键字', () => { test('应该支持多个关键字', () => {
expect( expect(
dynamicURL('http://api.com/users/name/:name/age/:age/list', { dynamicURL('http://api.com/tests/name/:name/type/:type/list', {
name: 'my', name: 'axios',
age: 18, type: 0,
}), }),
).toBe('http://api.com/users/name/my/age/18/list'); ).toBe('http://api.com/tests/name/axios/type/0/list');
}); });
}); });

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { captureError, cleanedStack } from 'scripts/test.utils'; import { captureError, checkStack } from 'scripts/test.utils';
import { assert, throwError, cleanStack } from 'src/helpers/error'; import { assert, throwError, cleanStack } from 'src/helpers/error';
describe('src/helpers/error.ts', () => { describe('src/helpers/error.ts', () => {
@ -9,25 +9,27 @@ describe('src/helpers/error.ts', () => {
test('第一个参数为 false 时应该抛出异常', () => { test('第一个参数为 false 时应该抛出异常', () => {
expect(() => assert(false, '')).toThrowError(); expect(() => assert(false, '')).toThrowError();
expect(cleanedStack(captureError(() => assert(false, '')))).toBeTruthy(); expect(checkStack(captureError(() => assert(false, '')))).toBeTruthy();
}); });
test('应该抛出异常', () => { test('应该抛出异常', () => {
expect(() => throwError('')).toThrowError('[axios-miniprogram]: '); expect(() => throwError('')).toThrowErrorMatchingInlineSnapshot(
expect(() => throwError('error')).toThrowError( '"[axios-miniprogram]: "',
'[axios-miniprogram]: error',
); );
expect(cleanedStack(captureError(() => throwError('error')))).toBeTruthy(); expect(() => throwError('error')).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: error"',
);
expect(checkStack(captureError(() => throwError('error')))).toBeTruthy();
}); });
test('应该清掉多余的错误栈', () => { test('应该清掉多余的错误栈', () => {
const ce = () => new Error(); const ce = () => new Error();
const error = ce(); const error = ce();
expect(cleanedStack(error)).toBeFalsy(); expect(checkStack(error)).toBeFalsy();
cleanStack(error); cleanStack(error);
expect(cleanedStack(error)).toBeTruthy(); expect(checkStack(error)).toBeTruthy();
}); });
}); });

View File

@ -3,31 +3,37 @@ import { ignore } from 'src/helpers/ignore';
describe('src/helpers/ignore.ts', () => { describe('src/helpers/ignore.ts', () => {
test('不应该改变传入的对象', () => { test('不应该改变传入的对象', () => {
expect( expect(ignore({ v1: 1 })).toEqual({ v1: 1 });
ignore({
v1: 1,
}),
).toEqual({
v1: 1,
});
}); });
test('应该忽略指定键值', () => { test('应该忽略指定键值', () => {
expect( const o = {
ignore( v1: 1,
{ v2: {},
v1: 1, v3: [],
v2: {}, };
v3: [],
v4: undefined, expect(ignore(o, 'v1')).toEqual({
v5: 5, v2: {},
v6: null, v3: [],
}, });
'v1', expect(ignore(o, 'v2')).toEqual({
'v2', v1: 1,
'v3', v3: [],
'v4', });
), expect(ignore(o, 'v3')).toEqual({
).toEqual({ v5: 5, v6: null }); v1: 1,
v2: {},
});
expect(ignore(o, 'v1', 'v2')).toEqual({
v3: [],
});
expect(ignore(o, 'v2', 'v3')).toEqual({
v1: 1,
});
expect(ignore(o, 'v1', 'v3')).toEqual({
v2: {},
});
expect(ignore(o, 'v1', 'v2', 'v3')).toEqual({});
}); });
}); });

View File

@ -4,17 +4,17 @@ import { isAbsoluteURL } from 'src/helpers/isAbsoluteURL';
describe('src/helpers/isAbsoluteURL.ts', () => { describe('src/helpers/isAbsoluteURL.ts', () => {
test('应该不是绝对路径', () => { test('应该不是绝对路径', () => {
expect(isAbsoluteURL('user')).toBeFalsy(); expect(isAbsoluteURL('user')).toBeFalsy();
expect(isAbsoluteURL('/user')).toBeFalsy(); expect(isAbsoluteURL('/test')).toBeFalsy();
expect(isAbsoluteURL('//user')).toBeFalsy(); expect(isAbsoluteURL('//test')).toBeFalsy();
expect(isAbsoluteURL('://user')).toBeFalsy(); expect(isAbsoluteURL('://test')).toBeFalsy();
}); });
test('应该是绝对路径', () => { test('应该是绝对路径', () => {
expect(isAbsoluteURL('http://user')).toBeTruthy(); expect(isAbsoluteURL('http://test')).toBeTruthy();
expect(isAbsoluteURL('HTTP://user')).toBeTruthy(); expect(isAbsoluteURL('HTTP://test')).toBeTruthy();
expect(isAbsoluteURL('https://user')).toBeTruthy(); expect(isAbsoluteURL('https://test')).toBeTruthy();
expect(isAbsoluteURL('custom://user')).toBeTruthy(); expect(isAbsoluteURL('custom://test')).toBeTruthy();
expect(isAbsoluteURL('custom-v1.0://user')).toBeTruthy(); expect(isAbsoluteURL('custom-v1.0://test')).toBeTruthy();
expect(isAbsoluteURL('custom_v1.0://user')).toBeTruthy(); expect(isAbsoluteURL('custom_v1.0://test')).toBeTruthy();
}); });
}); });

View File

@ -9,6 +9,6 @@
"noEmit": true, "noEmit": true,
"moduleResolution": "node" "moduleResolution": "node"
}, },
"include": ["./src", "./test", "./global.d.ts"], "include": ["./src", "./test", "./global.d.ts", "./global.variables.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }