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,
// 请求方法
method,
// 请求参数
params,
// 请求数据
data,
// 请求头 同 headers
@ -53,7 +55,7 @@ axios.defaults.adapter = function adapter(adapterConfig) {
return wx.downloadFile({
url,
method,
filePath: data.filePath,
filePath: params.filePath,
header: headers,
success,
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>;

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) {
const isDts = format === 'dts';
const output = {
file: resolvePath(format, isDts),
file: resolveOutput(format, isDts),
format: isDts ? 'es' : format,
name: pkg.name,
exports: 'default',
@ -51,7 +51,7 @@ function buildConfig(format) {
};
}
function resolvePath(format, isDts) {
function resolveOutput(format, isDts) {
return path.resolve(
distPath,
`${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));
}
export function captureError<T = any>(fn: () => void): T {
export function captureError<T = any>(fn: () => void) {
try {
fn();
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) {
return error.stack.indexOf('at') === error.stack.indexOf('at /');
}
@ -28,7 +28,7 @@ export function noop() {
return;
}
export function mockResponse(
export function mockResponseBase(
status: number,
statusText: string,
headers: AnyObject,
@ -42,12 +42,15 @@ export function mockResponse(
};
}
export function mockSuccess(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponse(200, 'OK', headers, data);
export function mockResponse(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponseBase(200, 'OK', headers, data);
}
export function mockFail(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponse(400, 'FAIL', headers, data);
export function mockResponseError(
headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponseBase(400, 'FAIL', headers, data);
}
export interface MockAdapterOptions {
@ -58,8 +61,8 @@ export interface MockAdapterOptions {
after?: () => void;
}
export function mockAdapter(
type: 'success' | 'fail',
export function mockAdapterBase(
type: 'success' | 'error' | 'fail' = 'success',
options: MockAdapterOptions = {},
) {
const { headers = {}, data = {}, delay = 0, before, after } = options;
@ -67,20 +70,30 @@ export function mockAdapter(
return (config: AxiosAdapterRequestConfig) => {
before?.(config);
setTimeout(() => {
if (type === 'success') {
config.success(mockSuccess(headers, data));
} else {
config.fail(mockFail(headers, data));
switch (type) {
case 'success':
config.success(mockResponse(headers, data));
break;
case 'error':
config.success(mockResponseError(headers, data));
break;
case 'fail':
config.fail(mockResponseError(headers));
break;
}
after?.();
}, delay);
};
}
export function mockAdapterSuccess(options: MockAdapterOptions = {}) {
return mockAdapter('success', options);
export function mockAdapter(options: MockAdapterOptions = {}) {
return mockAdapterBase('success', options);
}
export function mockAdapterError(options: MockAdapterOptions = {}) {
return mockAdapterBase('error', options);
}
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;
for (const alias of Axios.as) {
this[alias] = (url, config) => {
return this._req(alias, url, undefined, config);
this[alias] = (url, config = {}) => {
return this.request({
...config,
method: alias,
url,
});
};
}
for (const alias of Axios.pas) {
this[alias] = (url, params, config) => {
return this._req(alias, url, params, config);
this[alias] = (url, params, config = {}) => {
return this.request({
...config,
method: alias,
params,
url,
});
};
}
for (const alias of Axios.das) {
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>>;
}
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,
);
return request<TData>(config).then(
(response: AxiosResponse<TData>) => {
function transformer(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);
response.data = transformData(
response.data as AnyObject,
response.headers,
config.transformResponse,
) as TData;
transformer(response);
return response;
},
(reason: unknown) => {
})
.catch((reason: unknown) => {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (isPlainObject(reason) && isPlainObject(reason.response)) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse,
);
const response = (reason as AnyObject)?.response;
if (isPlainObject(response)) {
transformer(response as AxiosResponse<TData>);
}
}
throw config.errorHandler?.(reason) ?? reason;
},
);
});
}

View File

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

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 {
AxiosAdapterRequestConfig,
@ -42,10 +42,12 @@ export function request<TData = unknown>(
): Promise<AxiosResponse<TData>> {
return new Promise((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 不是一个 function');
assert(isString(config.url), 'url 不是一个 string');
assert(isString(config.method), 'method 不是一个 string');
const adapterConfig: AxiosAdapterRequestConfig = {
...config,
url: config.url ?? '',
url: config.url,
type: generateType(config),
method: config.method!.toUpperCase() as AxiosAdapterRequestMethod,
success,

View File

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

View File

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

View File

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

View File

@ -1,27 +1,27 @@
import { describe, test, expect } from 'vitest';
import { cleanedStack } from 'scripts/test.utils';
import { checkStack } from 'scripts/test.utils';
import { createError } from 'src/core/createError';
describe('src/core/createError.ts', () => {
test('应该支持空参数', () => {
const config = {};
const axiosError = createError('error', config);
const c = {};
const err = createError('error', c);
expect(axiosError.isAxiosError).toBeTruthy();
expect(axiosError.message).toBe('error');
expect(axiosError.config).toBe(config);
expect(cleanedStack(axiosError)).toBeTruthy();
expect(err.isAxiosError).toBeTruthy();
expect(err.message).toBe('error');
expect(err.config).toBe(c);
expect(checkStack(err)).toBeTruthy();
});
test('应该支持传入更多参数', () => {
const config = {};
const request = {};
const response = {};
const axiosError = createError('error', config, request, response as any);
const c = {};
const req = {};
const res = {};
const err = createError('error', c, req, res as any);
expect(axiosError.message).toBe('error');
expect(axiosError.config).toBe(config);
expect(axiosError.request).toBe(request);
expect(axiosError.response).toBe(response);
expect(err.message).toBe('error');
expect(err.config).toBe(c);
expect(err.request).toBe(req);
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 { mockAdapterSuccess } from 'scripts/test.utils';
import { generateType } from 'src/core/generateType';
import Axios from 'src/core/Axios';
import axios from 'src/axios';
describe('src/core/generateType.ts', () => {
test('应该是一个 reuqest', () => {
for (const alias of [...Axios.as, ...Axios.pas, ...Axios.das]) {
expect(generateType({ method: alias })).toBe('request');
axios({
adapter: mockAdapterSuccess({
before: (config) => {
expect(config.type).toBe('request');
},
}),
method: alias,
});
for (const a of [...Axios.as, ...Axios.pas, ...Axios.das]) {
expect(generateType({ method: a })).toBe('request');
}
});
test('应该是一个 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', () => {
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', () => {
test('应该支持空参数', () => {
expect(buildURL('/user')).toBe('/user');
expect(buildURL('/test')).toBe('/test');
});
test('应该清理哈希值', () => {
expect(buildURL('/user#hash')).toBe('/user');
expect(buildURL('/test#hash')).toBe('/test');
});
test('应该对参数进行系列化', () => {
expect(
buildURL('/user#hash', {
buildURL('/test#hash', {
v1: 1,
v2: undefined,
v3: null,
v4: '4',
v5: NaN,
}),
).toBe('/user?v1=1&v4=4');
).toBe('/test?v1=1&v4=4');
expect(
buildURL('/user?v1=1', {
buildURL('/test?v1=1', {
v2: 2,
}),
).toBe('/user?v1=1&v2=2');
).toBe('/test?v1=1&v2=2');
});
test('应该对数组进行系列化', () => {
expect(
buildURL('/user', {
buildURL('/test', {
arr: [1, 2],
}),
).toBe('/user?arr[]=1&arr[]=2');
).toBe('/test?arr[]=1&arr[]=2');
});
test('应该对对象进行系列化', () => {
expect(
buildURL('/user', {
buildURL('/test', {
obj: {
k1: 1,
k2: 2,
},
}),
).toBe('/user?obj[k1]=1&obj[k2]=2');
).toBe('/test?obj[k1]=1&obj[k2]=2');
});
test('应该对日期进行系列化', () => {
const date = new Date();
expect(buildURL('/user', { date })).toBe(
`/user?date=${date.toISOString()}`,
const d = new Date();
expect(buildURL('/test', { date: d })).toBe(
`/test?date=${d.toISOString()}`,
);
});
test('应该支持自定义序列化器', () => {
expect(buildURL('/user', {}, () => 'v1=1&v2=2')).toBe('/user?v1=1&v2=2');
expect(buildURL('/user?v1=1', {}, () => 'v2=2&v3=3')).toBe(
'/user?v1=1&v2=2&v3=3',
expect(buildURL('/test', {}, () => 'v1=1&v2=2')).toBe('/test?v1=1&v2=2');
expect(buildURL('/test?v1=1', {}, () => 'v2=2&v3=3')).toBe(
'/test?v1=1&v2=2&v3=3',
);
});
});

View File

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

View File

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

View File

@ -3,20 +3,20 @@ import { dynamicURL } from 'src/helpers/dynamicURL';
describe('src/helpers/dynamicURL.ts', () => {
test('应该替换关键字', () => {
expect(dynamicURL('http://api.com/user/:id', {})).toBe(
'http://api.com/user/undefined',
expect(dynamicURL('http://api.com/test/:id', {})).toBe(
'http://api.com/test/undefined',
);
expect(dynamicURL('http://api.com/user/:id', { id: 1 })).toBe(
'http://api.com/user/1',
expect(dynamicURL('http://api.com/test/:id', { id: 1 })).toBe(
'http://api.com/test/1',
);
});
test('应该支持多个关键字', () => {
expect(
dynamicURL('http://api.com/users/name/:name/age/:age/list', {
name: 'my',
age: 18,
dynamicURL('http://api.com/tests/name/:name/type/:type/list', {
name: 'axios',
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 { captureError, cleanedStack } from 'scripts/test.utils';
import { captureError, checkStack } from 'scripts/test.utils';
import { assert, throwError, cleanStack } from 'src/helpers/error';
describe('src/helpers/error.ts', () => {
@ -9,25 +9,27 @@ describe('src/helpers/error.ts', () => {
test('第一个参数为 false 时应该抛出异常', () => {
expect(() => assert(false, '')).toThrowError();
expect(cleanedStack(captureError(() => assert(false, '')))).toBeTruthy();
expect(checkStack(captureError(() => assert(false, '')))).toBeTruthy();
});
test('应该抛出异常', () => {
expect(() => throwError('')).toThrowError('[axios-miniprogram]: ');
expect(() => throwError('error')).toThrowError(
'[axios-miniprogram]: error',
expect(() => throwError('')).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: "',
);
expect(cleanedStack(captureError(() => throwError('error')))).toBeTruthy();
expect(() => throwError('error')).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: error"',
);
expect(checkStack(captureError(() => throwError('error')))).toBeTruthy();
});
test('应该清掉多余的错误栈', () => {
const ce = () => new Error();
const error = ce();
expect(cleanedStack(error)).toBeFalsy();
expect(checkStack(error)).toBeFalsy();
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', () => {
test('不应该改变传入的对象', () => {
expect(
ignore({
v1: 1,
}),
).toEqual({
v1: 1,
});
expect(ignore({ v1: 1 })).toEqual({ v1: 1 });
});
test('应该忽略指定键值', () => {
expect(
ignore(
{
v1: 1,
v2: {},
v3: [],
v4: undefined,
v5: 5,
v6: null,
},
'v1',
'v2',
'v3',
'v4',
),
).toEqual({ v5: 5, v6: null });
const o = {
v1: 1,
v2: {},
v3: [],
};
expect(ignore(o, 'v1')).toEqual({
v2: {},
v3: [],
});
expect(ignore(o, 'v2')).toEqual({
v1: 1,
v3: [],
});
expect(ignore(o, 'v3')).toEqual({
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', () => {
test('应该不是绝对路径', () => {
expect(isAbsoluteURL('user')).toBeFalsy();
expect(isAbsoluteURL('/user')).toBeFalsy();
expect(isAbsoluteURL('//user')).toBeFalsy();
expect(isAbsoluteURL('://user')).toBeFalsy();
expect(isAbsoluteURL('/test')).toBeFalsy();
expect(isAbsoluteURL('//test')).toBeFalsy();
expect(isAbsoluteURL('://test')).toBeFalsy();
});
test('应该是绝对路径', () => {
expect(isAbsoluteURL('http://user')).toBeTruthy();
expect(isAbsoluteURL('HTTP://user')).toBeTruthy();
expect(isAbsoluteURL('https://user')).toBeTruthy();
expect(isAbsoluteURL('custom://user')).toBeTruthy();
expect(isAbsoluteURL('custom-v1.0://user')).toBeTruthy();
expect(isAbsoluteURL('custom_v1.0://user')).toBeTruthy();
expect(isAbsoluteURL('http://test')).toBeTruthy();
expect(isAbsoluteURL('HTTP://test')).toBeTruthy();
expect(isAbsoluteURL('https://test')).toBeTruthy();
expect(isAbsoluteURL('custom://test')).toBeTruthy();
expect(isAbsoluteURL('custom-v1.0://test')).toBeTruthy();
expect(isAbsoluteURL('custom_v1.0://test')).toBeTruthy();
});
});

View File

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