diff --git a/scripts/test.utils.ts b/scripts/test.utils.ts new file mode 100644 index 0000000..92e97ea --- /dev/null +++ b/scripts/test.utils.ts @@ -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(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); +} diff --git a/src/core/cancel.ts b/src/core/cancel.ts index 0dbbcc9..317515c 100644 --- a/src/core/cancel.ts +++ b/src/core/cancel.ts @@ -37,11 +37,11 @@ export function isCancel(value: unknown): value is Cancel { export class CancelToken { private reason?: Cancel; - public listener: Promise; + public onCancel: Promise['then']; public constructor(executor: CancelExecutor) { let action!: CancelAction; - this.listener = new Promise((resolve) => { + const promise = new Promise((resolve) => { action = (message) => { if (this.reason) { return; @@ -53,6 +53,8 @@ export class CancelToken { }; }); + this.onCancel = promise.then.bind(promise); + executor(action); } diff --git a/src/core/request.ts b/src/core/request.ts index 7209264..4c5f81b 100644 --- a/src/core/request.ts +++ b/src/core/request.ts @@ -84,7 +84,7 @@ export function request( } if (isCancelToken(config.cancelToken)) { - config.cancelToken.listener.then((reason: unknown) => { + config.cancelToken.onCancel((reason: unknown) => { if (isPlainObject(adapterTask)) { tryToggleProgressUpdate(adapterConfig, adapterTask.offProgressUpdate); diff --git a/test/core/cancel.test.ts b/test/core/cancel.test.ts new file mode 100644 index 0000000..69cd3bb --- /dev/null +++ b/test/core/cancel.test.ts @@ -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(() => 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(); + }); +}); diff --git a/test/helpers/error.test.ts b/test/helpers/error.test.ts index 6a6f30d..423f49c 100644 --- a/test/helpers/error.test.ts +++ b/test/helpers/error.test.ts @@ -1,5 +1,5 @@ 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', () => { test('第一个参数为 true 时应该无事发生', () => {