refactor: 重建响应数据类型

pull/41/head
zjx0905 2023-04-09 15:20:10 +08:00
parent ee6a31b4bb
commit 4c75fa3d32
33 changed files with 544 additions and 305 deletions

View File

@ -34,6 +34,7 @@
"cz": "czg",
"build": "esno scripts/build.ts",
"build:asset": "esno scripts/build-asset.ts",
"watch": "pnpm build -a -w",
"release": "esno scripts/release.ts",
"publish:ci": "esno scripts/publish.ts",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",

View File

@ -28,7 +28,7 @@ export function noop() {
return;
}
export function mockResponseBase(
export function mockResponse(
status: number,
statusText: string,
headers: AnyObject,
@ -42,17 +42,6 @@ export function mockResponseBase(
};
}
export function mockResponse(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponseBase(200, 'OK', headers, data);
}
export function mockResponseError(
headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponseBase(400, 'FAIL', headers, data);
}
export interface MockAdapterOptions {
headers?: AnyObject;
data?: AnyObject;
@ -68,21 +57,33 @@ export function mockAdapterBase(
const { headers = {}, data = {}, delay = 0, before, after } = options;
return (config: AxiosAdapterRequestConfig) => {
let canceled = false;
before?.(config);
setTimeout(() => {
if (!canceled) {
switch (type) {
case 'success':
config.success(mockResponse(headers, data));
config.success(mockResponse(200, 'OK', headers, data));
break;
case 'error':
config.success(mockResponseError(headers, data));
config.success(mockResponse(500, 'ERROR', headers, data));
break;
case 'fail':
config.fail(mockResponseError(headers));
config.fail(mockResponse(400, 'FAIL', headers, data));
break;
}
after?.();
}
}, delay);
return {
abort() {
canceled = true;
},
};
};
}

View File

@ -9,7 +9,6 @@ import {
AxiosProgressCallback,
AxiosRequestFormData,
AxiosRequestHeaders,
AxiosResponse,
} from './core/Axios';
export type AxiosAdapterRequestType = 'request' | 'download' | 'upload';
@ -24,7 +23,7 @@ export type AxiosAdapterRequestMethod =
| 'TRACE'
| 'CONNECT';
export interface AxiosAdapterResponse<TData = unknown> extends AnyObject {
export interface AxiosAdapterResponse extends AnyObject {
/**
*
*/
@ -40,7 +39,7 @@ export interface AxiosAdapterResponse<TData = unknown> extends AnyObject {
/**
*
*/
data: TData;
data: string | ArrayBuffer | AnyObject;
}
export interface AxiosAdapterResponseError extends AnyObject {
@ -107,8 +106,8 @@ export interface AxiosAdapterRequestConfig extends AnyObject {
export interface AxiosAdapterBaseOptions extends AxiosAdapterRequestConfig {
header?: AxiosRequestHeaders;
success(response: unknown): void;
fail(error: unknown): void;
success(response: AxiosAdapterResponse): void;
fail(error: AxiosAdapterResponseError): void;
}
export interface AxiosAdapterUploadOptions
@ -239,10 +238,10 @@ export function createAdapter(platform: AxiosPlatform): AxiosAdapter {
download: AxiosAdapterDownload,
baseOptions: AxiosAdapterBaseOptions,
): AxiosAdapterTask {
const options = {
const options: AxiosAdapterDownloadOptions = {
...baseOptions,
filePath: baseOptions.params?.filePath,
success(response: AnyObject): void {
success(response): void {
injectDownloadData(response);
baseOptions.success(response);
},
@ -275,7 +274,7 @@ export function createAdapter(platform: AxiosPlatform): AxiosAdapter {
return {
...config,
header: config.headers,
success(response: AxiosResponse<unknown>): void {
success(response): void {
transformResult(response);
config.success(response);
},

View File

@ -61,7 +61,7 @@ function createInstance(defaults: AxiosRequestConfig) {
Object.assign(instance, context, {
// instance.fork 内部调用了 context 的私有方法
// 所以直接调用 instance.fork 会导致程序抛出无法访问私有方法的异常
// 所以直接调用 instance.fork 会导致程序抛出无法访问私有方法的异常
// instance.fork 调用时 this 重新指向 context解决此问题
fork: context.fork.bind(context),
});

View File

@ -11,7 +11,7 @@ import {
AxiosAdapterResponseError,
} from '../adapter';
import { CancelToken } from './cancel';
import dispatchRequest from './dispatchRequest';
import { dispatchRequest } from './dispatchRequest';
import InterceptorManager from './InterceptorManager';
import { AxiosTransformer } from './transformData';
import AxiosDomain from './AxiosDomain';
@ -79,6 +79,13 @@ export interface AxiosRequestFormData extends AnyObject {
export type AxiosRequestData = AnyObject | AxiosRequestFormData;
export type AxiosResponseData =
| undefined
| number
| string
| ArrayBuffer
| AnyObject;
export interface AxiosProgressEvent {
progress: number;
totalBytesSent: number;
@ -90,9 +97,8 @@ export interface AxiosProgressCallback {
}
export interface AxiosRequestConfig
extends Omit<
Partial<AxiosAdapterRequestConfig>,
'type' | 'method' | 'success' | 'fail'
extends Partial<
Omit<AxiosAdapterRequestConfig, 'type' | 'success' | 'fail'>
> {
/**
*
@ -102,6 +108,10 @@ export interface AxiosRequestConfig
*
*/
baseURL?: string;
/**
* URL
*/
url?: string;
/**
*
*/
@ -133,11 +143,11 @@ export interface AxiosRequestConfig
/**
*
*/
transformRequest?: AxiosTransformer | AxiosTransformer[];
transformRequest?: AxiosTransformer<AxiosRequestData>;
/**
*
*/
transformResponse?: AxiosTransformer | AxiosTransformer[];
transformResponse?: AxiosTransformer<AxiosResponseData>;
/**
*
*/
@ -160,8 +170,9 @@ export interface AxiosRequestConfig
validateStatus?: (status: number) => boolean;
}
export interface AxiosResponse<TData = unknown>
extends AxiosAdapterResponse<TData> {
export interface AxiosResponse<
TData extends AxiosResponseData = AxiosResponseData,
> extends Omit<AxiosAdapterResponse, 'data'> {
/**
*
*/
@ -170,6 +181,10 @@ export interface AxiosResponse<TData = unknown>
*
*/
request?: AxiosAdapterTask;
/**
*
*/
data: TData;
}
export interface AxiosResponseError extends AxiosAdapterResponseError {
@ -233,22 +248,17 @@ export default class Axios extends AxiosDomain {
);
}
#processRequest<TData = unknown>(config: AxiosRequestConfig) {
#processRequest(config: AxiosRequestConfig) {
const { request, response } = this.interceptors;
let promiseRequest = Promise.resolve(config);
request.forEach(({ resolved, rejected }) => {
promiseRequest = promiseRequest.then(
resolved,
rejected,
) as Promise<AxiosRequestConfig>;
promiseRequest = promiseRequest.then(resolved, rejected);
}, true);
let promiseResponse = promiseRequest.then(dispatchRequest);
response.forEach(({ resolved, rejected }) => {
promiseResponse = promiseResponse.then(resolved, rejected) as Promise<
AxiosResponse<unknown>
>;
promiseResponse = promiseResponse.then(resolved, rejected);
});
return promiseResponse as Promise<AxiosResponse<TData>>;
return promiseResponse;
}
}

View File

@ -1,16 +1,21 @@
import { isString, isUndefined } from '../helpers/isTypes';
import { deepMerge } from '../helpers/deepMerge';
import { mergeConfig } from './mergeConfig';
import { AxiosRequestConfig, AxiosRequestData, AxiosResponse } from './Axios';
import {
AxiosRequestConfig,
AxiosRequestData,
AxiosResponse,
AxiosResponseData,
} from './Axios';
export interface AxiosDomainRequest {
<TData = unknown>(
<TData extends AxiosResponseData>(
/**
*
*/
config: AxiosRequestConfig,
): Promise<AxiosResponse<TData>>;
<TData = unknown>(
<TData extends AxiosResponseData>(
/**
*
*/
@ -23,7 +28,7 @@ export interface AxiosDomainRequest {
}
export interface AxiosDomainAsRequest {
<TData = unknown>(
<TData extends AxiosResponseData>(
/**
*
*/
@ -36,7 +41,7 @@ export interface AxiosDomainAsRequest {
}
export interface AxiosDomainAsRequestWithParams {
<TData = unknown>(
<TData extends AxiosResponseData>(
/**
*
*/
@ -53,7 +58,7 @@ export interface AxiosDomainAsRequestWithParams {
}
export interface AxiosDomainAsRequestWithData {
<TData = unknown>(
<TData extends AxiosResponseData>(
/**
*
*/

View File

@ -2,17 +2,17 @@ export interface InterceptorResolved<T = unknown> {
(value: T): T | Promise<T>;
}
export interface InterceptorRejected {
(error: unknown): unknown | Promise<unknown>;
export interface InterceptorRejected<T = unknown> {
(error: unknown): T | Promise<T>;
}
export interface Interceptor<T = unknown> {
resolved: InterceptorResolved<T>;
rejected?: InterceptorRejected;
rejected?: InterceptorRejected<T>;
}
export interface InterceptorExecutor {
(interceptor: Interceptor): void;
export interface InterceptorExecutor<T = unknown> {
(interceptor: Interceptor<T>): void;
}
export default class InterceptorManager<T = unknown> {
@ -22,7 +22,7 @@ export default class InterceptorManager<T = unknown> {
use(
resolved: InterceptorResolved<T>,
rejected?: InterceptorRejected,
rejected?: InterceptorRejected<T>,
): number {
this.#interceptors[++this.#id] = {
resolved,
@ -36,8 +36,8 @@ export default class InterceptorManager<T = unknown> {
delete this.#interceptors[id];
}
forEach(executor: InterceptorExecutor, reverse?: boolean): void {
let interceptors: Interceptor<any>[] = Object.values(this.#interceptors);
forEach(executor: InterceptorExecutor<T>, reverse?: boolean): void {
let interceptors: Interceptor<T>[] = Object.values(this.#interceptors);
if (reverse) interceptors = interceptors.reverse();
interceptors.forEach(executor);
}

View File

@ -30,7 +30,7 @@ export function createError(
config: AxiosRequestConfig,
response: AxiosErrorResponse,
request: AxiosAdapterTask,
): AxiosError {
) {
const axiosError = new AxiosError(message, config, response, request);
cleanStack(axiosError);
return axiosError;

View File

@ -14,29 +14,30 @@ function throwIfCancellationRequested(config: AxiosRequestConfig) {
}
}
export default function dispatchRequest<TData = unknown>(
config: AxiosRequestConfig,
): Promise<AxiosResponse> {
export function dispatchRequest(config: AxiosRequestConfig) {
throwIfCancellationRequested(config);
const { transformRequest, transformResponse } = config;
config.url = transformURL(config);
config.method = config.method ?? 'get';
config.headers = flattenHeaders(config);
transform(config, transformRequest);
transformer(config, transformRequest);
function onSuccess(response: AxiosResponse<TData>) {
function onSuccess(response: AxiosResponse) {
throwIfCancellationRequested(config);
transform(response, transformResponse);
transformer(response, transformResponse);
return response;
}
function onError(reason: unknown) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (isAxiosError(reason)) {
transform(reason.response as AxiosResponse<TData>, transformResponse);
transformer(reason.response as AxiosResponse, transformResponse);
}
}
@ -47,16 +48,16 @@ export default function dispatchRequest<TData = unknown>(
return Promise.reject(reason);
}
function transform<TData = unknown>(
target: AxiosRequestConfig | AxiosResponse<TData>,
transformer?: AxiosTransformer | AxiosTransformer[],
function transformer<TData = unknown>(
targetObject: { data?: TData; headers?: AnyObject },
transformer?: AxiosTransformer<TData>,
) {
target.data = transformData(
target.data as AnyObject,
target.headers,
targetObject.data = transformData(
targetObject.data,
targetObject.headers,
transformer,
) as TData;
);
}
return request<TData>(config).then(onSuccess).catch(onError);
return request(config).then(onSuccess).catch(onError);
}

View File

@ -4,9 +4,7 @@ import { AxiosRequestConfig } from './Axios';
const postRE = /^POST$/i;
const getRE = /^GET$/i;
export function generateType(
config: AxiosRequestConfig,
): AxiosAdapterRequestType {
export function generateType(config: AxiosRequestConfig) {
let requestType: AxiosAdapterRequestType = 'request';
if (config.upload && postRE.test(config.method!)) {

View File

@ -17,7 +17,7 @@ const deepMergeConfigMap: Record<string, boolean> = {
export function mergeConfig(
config1: AxiosRequestConfig = {},
config2: AxiosRequestConfig = {},
): AxiosRequestConfig {
) {
const config: AxiosRequestConfig = {};
// 所有已知键名

View File

@ -10,6 +10,7 @@ import {
AxiosProgressCallback,
AxiosRequestConfig,
AxiosResponse,
AxiosResponseData,
AxiosResponseError,
} from './Axios';
import { isCancelToken } from './cancel';
@ -38,16 +39,14 @@ function tryToggleProgressUpdate(
}
}
export function request<TData = unknown>(
config: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return new Promise((resolve, reject) => {
export function request(config: AxiosRequestConfig) {
return new Promise<AxiosResponse>((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 不是一个 function');
assert(isString(config.url), 'url 不是一个 string');
const adapterConfig: AxiosAdapterRequestConfig = {
...config,
url: config.url,
url: config.url!,
type: generateType(config),
method: config.method!.toUpperCase() as AxiosAdapterRequestMethod,
success,
@ -56,10 +55,11 @@ export function request<TData = unknown>(
const adapterTask = config.adapter!(adapterConfig);
function success(_: AxiosAdapterResponse<TData>): void {
const response = _ as AxiosResponse<TData>;
function success(_: AxiosAdapterResponse): void {
const response = _ as AxiosResponse;
response.config = config;
response.request = adapterTask;
if (config.validateStatus?.(response.status) ?? true) {
resolve(response);
} else {
@ -72,7 +72,7 @@ export function request<TData = unknown>(
responseError.isFail = true;
responseError.config = config;
responseError.request = adapterTask;
catchError(responseError.statusText, responseError);
catchError('request fail', responseError);
}
function catchError(

View File

@ -1,35 +1,38 @@
import { isArray, isUndefined } from '../helpers/isTypes';
import { AxiosRequestData } from './Axios';
import { isArray, isFunction } from '../helpers/isTypes';
export interface AxiosTransformer {
export interface AxiosTransformCallback<TData = unknown> {
(
/**
*
*/
data?: AxiosRequestData,
data?: TData,
/**
*
*/
headers?: AnyObject,
): AxiosRequestData;
): TData | undefined;
}
export function transformData(
data?: AxiosRequestData,
export type AxiosTransformer<TData = unknown> =
| AxiosTransformCallback<TData>
| AxiosTransformCallback<TData>[];
export function transformData<TData = unknown>(
data?: TData,
headers?: AnyObject,
transforms?: AxiosTransformer | AxiosTransformer[],
): AxiosRequestData | undefined {
if (isUndefined(transforms)) {
return data;
}
transforms?: AxiosTransformer<TData>,
) {
if (!isArray(transforms)) {
if (isFunction(transforms)) {
transforms = [transforms];
} else {
transforms = [];
}
}
transforms.forEach((transform: AxiosTransformer) => {
transforms.forEach((transform) => {
data = transform(data, headers);
});
return data;
return data as TData;
}

View File

@ -4,7 +4,7 @@ import { dynamicURL } from '../helpers/dynamicURL';
import { isAbsoluteURL } from '../helpers/isAbsoluteURL';
import { AxiosRequestConfig } from './Axios';
export function transformURL(config: AxiosRequestConfig): string {
export function transformURL(config: AxiosRequestConfig) {
let url = config.url ?? '';
if (!isAbsoluteURL(url)) url = combineURL(config.baseURL ?? '', url);

View File

@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { getAdapterDefault } from 'src/adapter';
import { getAdapterDefault } from '@/adapter';
const platforms = [
'uni',

View File

@ -1,24 +1,29 @@
import { describe, test, expect, vi } from 'vitest';
import { createAdapter } from 'src/adapter';
import { noop } from 'scripts/test.utils';
import { AxiosPlatform, createAdapter } from '@/adapter';
describe('src/adapter.ts', () => {
test('应该抛出异常', () => {
expect(() =>
createAdapter(undefined as any),
createAdapter(undefined as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: platform 不是一个 object"',
);
expect(() => createAdapter({} as any)).toThrowErrorMatchingInlineSnapshot(
expect(() =>
createAdapter({} as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: request 不是一个 function"',
);
expect(() =>
createAdapter({ request: vi.fn() } as any),
createAdapter({ request: vi.fn() } as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: upload 不是一个 function"',
);
expect(() =>
createAdapter({ request: vi.fn(), upload: vi.fn() } as any),
createAdapter({
request: vi.fn(),
upload: vi.fn(),
} as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: download 不是一个 function"',
);

View File

@ -1,136 +1,17 @@
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { mockAdapter } from 'scripts/test.utils';
import Axios from '../src/core/Axios';
import { CancelToken, isCancel } from '../src/core/cancel';
import { isAxiosError } from '../src/core/createError';
import { createAdapter } from '../src/adapter';
import defaults from '../src/defaults';
import axios from '../src/axios';
import { describe, test, expect } from 'vitest';
import Axios from '@/core/Axios';
import { CancelToken, isCancel } from '@/core/cancel';
import { isAxiosError } from '@/core/createError';
import { createAdapter } from '@/adapter';
import axios from '@/axios';
describe('src/axios.ts', () => {
const data = {
result: null,
};
beforeAll(() => {
axios.defaults.baseURL = 'http://api.com';
});
afterAll(() => {
axios.defaults.baseURL = undefined;
});
test('应该有这些静态属性', () => {
expect(axios.defaults).toBe(defaults);
expect(axios.Axios).toBe(Axios);
expect(axios.CancelToken).toBe(CancelToken);
expect(axios.create).toBeTypeOf('function');
expect(axios.createAdapter).toBe(createAdapter);
expect(axios.isCancel).toBe(isCancel);
expect(axios.isAxiosError).toBe(isAxiosError);
expect(axios.interceptors).toBeTypeOf('object');
expect(axios.create).toBeTypeOf('function');
expect(axios.request).toBeTypeOf('function');
expect(axios.getUri).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(axios[k]).toBeTypeOf('function');
});
});
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
});
});

125
test/axios.instance.test.ts Normal file
View File

@ -0,0 +1,125 @@
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { mockAdapter } from 'scripts/test.utils';
import Axios from '@/core/Axios';
import defaults from '@/defaults';
import axios from '@/axios';
describe('src/axios.ts', () => {
const data = {
result: null,
};
beforeAll(() => {
axios.defaults.baseURL = 'http://api.com';
});
afterAll(() => {
axios.defaults.baseURL = undefined;
});
test('应该有这些实例属性及方法', () => {
expect(axios.defaults).toBe(defaults);
expect(axios.interceptors).toBeTypeOf('object');
expect(axios.getUri).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function');
expect(axios.request).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(axios[k]).toBeTypeOf('function');
});
});
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
});
});

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest';
import axios from 'src/axios';
import { mockAdapter, mockAdapterError } from 'scripts/test.utils';
import axios from '@/axios';
describe('src/axios.ts', () => {
test('应该处理成功和失败', () => {

View File

@ -1,27 +1,133 @@
import { describe, test, expect } from 'vitest';
import Axios from 'src/core/Axios';
import { mockAdapter } from 'scripts/test.utils';
import Axios from '@/core/Axios';
import AxiosDomain from '@/core/AxiosDomain';
describe('src/core/Axios.ts', () => {
const data = {
result: null,
};
const axios = new Axios({
baseURL: 'http://api.com',
});
test('应该继承自 AxiosDomain', () => {
expect(new Axios() instanceof AxiosDomain).toBeTruthy();
});
test('应该有这些静态属性', () => {
expect(Axios.as).toEqual(['options', 'trace', 'connect']);
expect(Axios.asp).toEqual(['head', 'get', 'delete']);
expect(Axios.asd).toEqual(['post', 'put']);
});
test('应该有这些实例属性', () => {
test('应该有这些实例属性及方法', () => {
const c = {
baseURL: 'http://api.com',
};
const a = new Axios(c);
expect(a.defaults).toEqual(c);
expect(a.interceptors).toBeTypeOf('object');
expect(a.request).toBeTypeOf('function');
expect(a.getUri).toBeTypeOf('function');
expect(a.fork).toBeTypeOf('function');
expect(axios.defaults).toEqual(c);
expect(axios.interceptors).toBeTypeOf('object');
expect(axios.request).toBeTypeOf('function');
expect(axios.getUri).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(a[k]).toBeTypeOf('function');
expect(axios[k]).toBeTypeOf('function');
});
});
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
});
});

View File

@ -1,6 +1,7 @@
import { describe, test, expect, vi } from 'vitest';
import AxiosDomain, { AxiosDomainRequest } from 'src/core/AxiosDomain';
import { ignore } from 'src/helpers/ignore';
import { ignore } from '@/helpers/ignore';
import AxiosDomain from '@/core/AxiosDomain';
import { AxiosResponse } from '@/core/Axios';
describe('src/core/AxiosDomain.ts', () => {
test('应该有这些静态属性', () => {
@ -48,6 +49,45 @@ describe('src/core/AxiosDomain.ts', () => {
});
test('应该可以调用这些方法', () => {
const cb = vi.fn();
const d = {
baseURL: 'http://api.com',
};
const u = 'test';
const c = {
params: {
id: 1,
},
data: {
id: 1,
},
};
const a = new AxiosDomain(d, async (config) => {
cb();
expect(config.baseURL).toBe(d.baseURL);
expect(config.url).toBe(u);
expect(config.params).toEqual(c.params);
expect(config.data).toEqual(c.data);
return {} as AxiosResponse;
});
a.request(u, c);
AxiosDomain.as.forEach((k) => a[k](u, c));
AxiosDomain.asp.forEach((k) => a[k](u, c.params, ignore(c, 'params')));
AxiosDomain.asd.forEach((k) => a[k](u, c.data, ignore(c, 'data')));
const l =
AxiosDomain.as.length +
AxiosDomain.asp.length +
AxiosDomain.asd.length +
1;
expect(cb.mock.calls.length).toBe(l);
});
test('应该可以直接传入 config 调用这些方法', () => {
const cb = vi.fn();
const d = {
baseURL: 'http://api.com',
@ -61,14 +101,16 @@ describe('src/core/AxiosDomain.ts', () => {
id: 1,
},
};
const a = new AxiosDomain(d, ((config) => {
const a = new AxiosDomain(d, async (config) => {
cb();
expect(config.baseURL).toBe(d.baseURL);
expect(config.url).toBe(c.url);
expect(config.params).toEqual(c.params);
expect(config.data).toEqual(c.data);
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
a.request(c);
@ -107,7 +149,7 @@ describe('src/core/AxiosDomain.ts', () => {
},
};
const a = new AxiosDomain(d, ((config) => {
const a = new AxiosDomain(d, async (config) => {
expect(config.params).toEqual({
v1: 1,
v2: {
@ -116,7 +158,9 @@ describe('src/core/AxiosDomain.ts', () => {
},
v3: 3,
});
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
AxiosDomain.asp.forEach((k) => a[k]('test', p, c));
});
@ -140,7 +184,7 @@ describe('src/core/AxiosDomain.ts', () => {
},
};
const a = new AxiosDomain(ds, ((config) => {
const a = new AxiosDomain(ds, async (config) => {
expect(config.data).toEqual({
v1: 1,
v2: {
@ -149,7 +193,9 @@ describe('src/core/AxiosDomain.ts', () => {
},
v3: 3,
});
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
AxiosDomain.asd.forEach((k) => a[k]('test', d, c));
});

View File

@ -1,5 +1,5 @@
import { describe, test, expect, vi } from 'vitest';
import InterceptorManager from 'src/core/InterceptorManager';
import InterceptorManager from '@/core/InterceptorManager';
describe('src/core/InterceptorManager.ts', () => {
test('应该有这些实例属性', () => {

View File

@ -7,12 +7,7 @@ import {
asyncTimeout,
} from 'scripts/test.utils';
import axios from 'src/axios';
import {
Cancel,
isCancel,
CancelToken,
isCancelToken,
} from '../../src/core/cancel';
import { Cancel, isCancel, CancelToken, isCancelToken } from '@/core/cancel';
describe('src/helpers/cancel.ts', () => {
test('应该支持空参数', () => {

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest';
import { checkStack } from 'scripts/test.utils';
import { createError, isAxiosError } from 'src/core/createError';
import { createError, isAxiosError } from '@/core/createError';
describe('src/core/createError.ts', () => {
test('应该支持空参数', () => {

View File

@ -0,0 +1,8 @@
import { describe, test, expect } from 'vitest';
import { dispatchRequest } from 'src/core/dispatchRequest';
describe('src/core/dispatchRequest.ts', () => {
test('应该有这些实例属性', () => {
expect(dispatchRequest).toBeTypeOf('function');
});
});

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest';
import { flattenHeaders } from 'src/core/flattenHeaders';
import Axios from 'src/core/Axios';
import { flattenHeaders } from '@/core/flattenHeaders';
import Axios from '@/core/Axios';
describe('src/core/flattenHeaders.ts', () => {
const keys = [...Axios.as, ...Axios.asp, ...Axios.asd];

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest';
import { generateType } from 'src/core/generateType';
import Axios from 'src/core/Axios';
import { generateType } from '@/core/generateType';
import Axios from '@/core/Axios';
describe('src/core/generateType.ts', () => {
test('应该是一个 reuqest', () => {

View File

@ -1,7 +1,7 @@
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';
import { ignore } from '@/helpers/ignore';
import { mergeConfig } from '@/core/mergeConfig';
import { CancelToken } from '@/core/cancel';
describe('src/core/mergeConfig.ts', () => {
test('应该支持空参数', () => {

View File

@ -1,10 +1,10 @@
import { describe, test, expect } from 'vitest';
import { request } from 'src/core/request';
import {
mockAdapter,
mockAdapterError,
mockAdapterFail,
} from 'scripts/test.utils';
import { request } from '@/core/request';
describe('src/core/request.ts', () => {
test('应该抛出异常', () => {
@ -18,14 +18,25 @@ describe('src/core/request.ts', () => {
);
});
test('应该能够取到数据', async () => {
await expect(
request({
test('应该正确的响应请求', async () => {
const s = request({
adapter: mockAdapter(),
url: '/test',
method: 'get',
}),
).resolves.toMatchInlineSnapshot(`
});
const e = request({
adapter: mockAdapterError(),
url: '/test',
method: 'get',
validateStatus: () => false,
});
const f = request({
adapter: mockAdapterFail(),
url: '/test',
method: 'get',
});
await expect(s).resolves.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
@ -34,19 +45,60 @@ describe('src/core/request.ts', () => {
},
"data": {},
"headers": {},
"request": undefined,
"request": {
"abort": [Function],
},
"status": 200,
"statusText": "OK",
}
`);
await expect(
request({
adapter: mockAdapterError(),
url: '/test',
method: 'get',
}),
).resolves.toMatchInlineSnapshot(`
await expect(e).rejects.toMatchInlineSnapshot(
'[Error: validate status fail]',
);
await expect(e.catch((e) => Object.assign({}, e))).resolves
.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
"validateStatus": [Function],
},
"request": {
"abort": [Function],
},
"response": {
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
"validateStatus": [Function],
},
"data": {},
"headers": {},
"request": {
"abort": [Function],
},
"status": 500,
"statusText": "ERROR",
},
}
`);
await expect(f).rejects.toMatchInlineSnapshot('[Error: request fail]');
await expect(f.catch((e) => Object.assign({}, e))).resolves
.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
},
"request": {
"abort": [Function],
},
"response": {
"config": {
"adapter": [Function],
"method": "get",
@ -54,17 +106,14 @@ describe('src/core/request.ts', () => {
},
"data": {},
"headers": {},
"request": undefined,
"isFail": true,
"request": {
"abort": [Function],
},
"status": 400,
"statusText": "FAIL",
},
}
`);
await expect(
request({
adapter: mockAdapterFail(),
url: '/test',
method: 'get',
}),
).rejects.toThrowErrorMatchingInlineSnapshot('"FAIL"');
});
});

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest';
import { transformData } from 'src/core/transformData';
import { transformData } from '@/core/transformData';
describe('src/core/transformData.ts', () => {
test('应该支持空配置', () => {

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest';
import { transformURL } from 'src/core/transformURL';
import { transformURL } from '@/core/transformURL';
describe('src/core/transformURL.ts', () => {
test('应该支持空配置', () => {

View File

@ -7,7 +7,10 @@
"module": "ESNext",
"strict": true,
"noEmit": true,
"moduleResolution": "node"
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["./src", "./test", "./global.d.ts", "./global.variables.d.ts"],
"exclude": ["node_modules"]

View File

@ -6,6 +6,9 @@ export default defineConfig({
test: {
root: resolve(__dirname),
globals: true,
alias: {
'@': resolve(__dirname, 'src'),
},
include: ['./test/**/*.test.ts'],
},
});