test: 测试 cancel

pull/41/head
zjx0905 2023-03-29 22:23:36 +08:00
parent 0d1e1c18e8
commit 7a61fda10e
5 changed files with 182 additions and 4 deletions

50
scripts/test.utils.ts Normal file
View File

@ -0,0 +1,50 @@
export function asyncNext() {
return Promise.resolve().then;
}
export function asyncTimeout(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
export function captureError<T = any>(fn: () => void): T {
try {
fn();
throw new Error('fn not fail...');
} catch (err) {
return err as T;
}
}
export function noop() {
return;
}
export function mockResponse(
status: number,
statusText: string,
headers: AnyObject,
data: AnyObject,
) {
return {
status,
statusText,
headers,
data,
};
}
export function mockSuccessResponse(
headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponse(200, 'OK', headers, data);
}
export function mockFailResponse(
headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponse(400, 'FAIL', headers, data);
}

View File

@ -37,11 +37,11 @@ export function isCancel(value: unknown): value is Cancel {
export class CancelToken { export class CancelToken {
private reason?: Cancel; private reason?: Cancel;
public listener: Promise<Cancel>; public onCancel: Promise<Cancel>['then'];
public constructor(executor: CancelExecutor) { public constructor(executor: CancelExecutor) {
let action!: CancelAction; let action!: CancelAction;
this.listener = new Promise<Cancel>((resolve) => { const promise = new Promise<Cancel>((resolve) => {
action = (message) => { action = (message) => {
if (this.reason) { if (this.reason) {
return; return;
@ -53,6 +53,8 @@ export class CancelToken {
}; };
}); });
this.onCancel = promise.then.bind(promise);
executor(action); executor(action);
} }

View File

@ -84,7 +84,7 @@ export function request<TData = unknown>(
} }
if (isCancelToken(config.cancelToken)) { if (isCancelToken(config.cancelToken)) {
config.cancelToken.listener.then((reason: unknown) => { config.cancelToken.onCancel((reason: unknown) => {
if (isPlainObject(adapterTask)) { if (isPlainObject(adapterTask)) {
tryToggleProgressUpdate(adapterConfig, adapterTask.offProgressUpdate); tryToggleProgressUpdate(adapterConfig, adapterTask.offProgressUpdate);

126
test/core/cancel.test.ts Normal file
View File

@ -0,0 +1,126 @@
import { describe, test, expect, vi } from 'vitest';
import {
asyncNext,
captureError,
mockSuccessResponse,
noop,
asyncTimeout,
} from 'scripts/test.utils';
import axios from 'src/axios';
import { Cancel, isCancel, CancelToken, isCancelToken } from 'src/core/cancel';
describe('测试 src/helpers/cancel.ts', () => {
test('应该支持空参数', () => {
const cancel = new Cancel();
expect(cancel.message).toBeUndefined();
expect(cancel.toString()).toBe('Cancel');
});
test('传入参数时应该有正确的返回结果', () => {
const cancel = new Cancel('error');
expect(cancel.message).toBe('error');
expect(cancel.toString()).toBe('Cancel: error');
});
test('应该正确判断 Cancel', () => {
expect(isCancel(undefined)).toBeFalsy();
expect(isCancel({})).toBeFalsy();
expect(new Cancel()).toBeTruthy();
});
test('应该可以取消', () => {
let cancelAction!: () => void;
const cancelToken = new CancelToken((action) => {
cancelAction = action;
});
expect(cancelToken.throwIfRequested()).toBeUndefined();
cancelAction();
expect(() => cancelToken.throwIfRequested()).toThrowError();
});
test('应该抛出正确的异常信息', async () => {
let cancelAction!: (msg: string) => void;
const cancelToken = new CancelToken((action) => {
cancelAction = action;
});
cancelAction('stop');
const error = captureError<Cancel>(() => cancelToken.throwIfRequested());
expect(error.message).toBe('stop');
expect(error.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();
cancelAction();
expect(canceled).not.toBeCalled();
await asyncNext();
expect(canceled).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy();
});
test('应该正确判断 CancelToken', () => {
expect(isCancelToken(undefined)).toBeFalsy();
expect(isCancelToken({})).toBeFalsy();
expect(isCancelToken(new CancelToken(noop))).toBeTruthy();
});
test('应该有正确返回结果', () => {
const source = CancelToken.source();
expect(source.cancel).toBeTypeOf('function');
expect(isCancelToken(source.token)).toBeTruthy();
});
test('应该可以取消', () => {
const source = CancelToken.source();
expect(source.token.throwIfRequested()).toBeUndefined();
source.cancel();
expect(() => source.token.throwIfRequested()).toThrowError();
});
test('应该可以在请求发出之前取消', async () => {
const canceled = vi.fn();
const source = CancelToken.source();
source.cancel();
axios({
adapter: ({ success }) => success(mockSuccessResponse()),
cancelToken: source.token,
}).catch(canceled);
await asyncTimeout();
expect(canceled).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy();
});
test('应该可以在请求发出之后取消', async () => {
const canceled = vi.fn();
const source = CancelToken.source();
axios({
adapter: ({ success }) => success(mockSuccessResponse()),
cancelToken: source.token,
}).catch(canceled);
source.cancel();
await asyncTimeout();
expect(canceled).toBeCalled();
expect(isCancel(canceled.mock.calls[0][0])).toBeTruthy();
});
});

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { assert, throwError } from '../../src/helpers/error'; import { assert, throwError } from 'src/helpers/error';
describe('测试 src/helpers/error.ts', () => { describe('测试 src/helpers/error.ts', () => {
test('第一个参数为 true 时应该无事发生', () => { test('第一个参数为 true 时应该无事发生', () => {