From 2c6d5656c871264bf7b9ac90712d799f75fd8947 Mon Sep 17 00:00:00 2001 From: maroon1 Date: Sat, 15 Jul 2023 14:19:03 +0800 Subject: [PATCH] fix: rethrow error in ActionButton's onOk callback (#43536) --- components/_util/ActionButton.tsx | 6 +- components/modal/__tests__/confirm.test.tsx | 90 ++++++++++++++++----- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/components/_util/ActionButton.tsx b/components/_util/ActionButton.tsx index 433cd68883..499fcd40ed 100644 --- a/components/_util/ActionButton.tsx +++ b/components/_util/ActionButton.tsx @@ -20,7 +20,7 @@ function isThenable(thing?: PromiseLike): boolean { return !!(thing && !!thing.then); } -const ActionButton: React.FC = props => { +const ActionButton: React.FC = (props) => { const clickedRef = React.useRef(false); const ref = React.useRef(null); const [loading, setLoading] = useState(false); @@ -55,12 +55,10 @@ const ActionButton: React.FC = props => { clickedRef.current = false; }, (e: Error) => { - // Emit error when catch promise reject - // eslint-disable-next-line no-console - console.error(e); // See: https://github.com/ant-design/ant-design/issues/6183 setLoading(false, true); clickedRef.current = false; + return Promise.reject(e); }, ); }; diff --git a/components/modal/__tests__/confirm.test.tsx b/components/modal/__tests__/confirm.test.tsx index 57b480f6e8..5b1a6f396a 100644 --- a/components/modal/__tests__/confirm.test.tsx +++ b/components/modal/__tests__/confirm.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import TestUtils from 'react-dom/test-utils'; import type { ModalFuncProps } from '..'; import Modal from '..'; -import { waitFakeTimer, act } from '../../../tests/utils'; +import { act, waitFakeTimer } from '../../../tests/utils'; import ConfigProvider from '../../config-provider'; import type { ModalFunc } from '../confirm'; import destroyFns from '../destroyFns'; @@ -18,10 +18,49 @@ const { confirm } = Modal; jest.mock('rc-motion'); +(global as any).injectPromise = false; +(global as any).rejectPromise = null; + +jest.mock('../../_util/ActionButton', () => { + const ActionButton = jest.requireActual('../../_util/ActionButton').default; + return (props: any) => { + const { actionFn } = props; + let mockActionFn: any = actionFn; + if (actionFn && (global as any).injectPromise) { + mockActionFn = (...args: any) => { + let ret = actionFn(...args); + + if (ret.then) { + let resolveFn: any; + let rejectFn: any; + + ret = ret.then( + (v: any) => { + resolveFn?.(v); + }, + (e: any) => { + rejectFn?.(e)?.catch((err: Error) => { + (global as any).rejectPromise = err; + }); + }, + ); + ret.then = (resolve: any, reject: any) => { + resolveFn = resolve; + rejectFn = reject; + }; + } + + return ret; + }; + } + return ; + }; +}); + describe('Modal.confirm triggers callbacks correctly', () => { // Inject CSSMotion to replace with No transition support const MockCSSMotion = genCSSMotion(false); - Object.keys(MockCSSMotion).forEach(key => { + Object.keys(MockCSSMotion).forEach((key) => { (CSSMotion as any)[key] = (MockCSSMotion as any)[key]; }); @@ -56,6 +95,11 @@ describe('Modal.confirm triggers callbacks correctly', () => { }; /* eslint-enable */ + beforeEach(() => { + (global as any).injectPromise = false; + (global as any).rejectPromise = null; + }); + beforeAll(() => { jest.useFakeTimers(); }); @@ -169,6 +213,8 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); it('should emit error when onOk return Promise.reject', async () => { + (global as any).injectPromise = true; + const error = new Error('something wrong'); await open({ onOk: () => Promise.reject(error), @@ -179,7 +225,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { // wait promise await waitFakeTimer(); - expect(errorSpy).toHaveBeenCalledWith(error); + expect((global as any).rejectPromise instanceof Error).toBeTruthy(); }); it('shows animation when close', async () => { @@ -214,7 +260,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('should close modals when click confirm button', () => { - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { it(type, async () => { Modal[type]?.({ title: 'title', content: 'content' }); await waitFakeTimer(); @@ -260,12 +306,12 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('should not close modals when click confirm button when onOk has argument', () => { - (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { + (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(type, async () => { Modal[type]?.({ title: 'title', content: 'content', - onOk: _ => null, // eslint-disable-line no-unused-vars + onOk: (_) => null, // eslint-disable-line no-unused-vars }); await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); @@ -278,7 +324,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('could be update by new config', () => { - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { it(type, async () => { const instance = Modal[type]?.({ title: 'title', @@ -305,7 +351,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('could be update by call function', () => { - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { it(type, async () => { const instance = Modal[type]?.({ title: 'title', @@ -318,7 +364,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { 'ant-btn-loading', ); expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].style.color).toBe('red'); - instance.update(prevConfig => ({ + instance.update((prevConfig) => ({ ...prevConfig, okButtonProps: { ...prevConfig.okButtonProps, @@ -341,7 +387,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('could be destroy', () => { - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { it(type, async () => { const instance = Modal[type]?.({ title: 'title', @@ -359,7 +405,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { it('could be Modal.destroyAll', async () => { // Show - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { Modal[type]?.({ title: 'title', content: 'content', @@ -368,7 +414,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); - ['info', 'success', 'warning', 'error'].forEach(type => { + ['info', 'success', 'warning', 'error'].forEach((type) => { expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); }); @@ -377,7 +423,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); - ['info', 'success', 'warning', 'error'].forEach(type => { + ['info', 'success', 'warning', 'error'].forEach((type) => { expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); }); }); @@ -402,7 +448,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); const instances: ReturnType[] = []; - (['info', 'success', 'warning', 'error'] as const).forEach(type => { + (['info', 'success', 'warning', 'error'] as const).forEach((type) => { const instance = Modal[type]?.({ title: 'title', content: 'content', @@ -560,13 +606,13 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); describe('the callback close should be a method when onCancel has a close parameter', () => { - (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { + (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`click the close icon to trigger ${type} onCancel`, async () => { const mock = jest.fn(); Modal[type]?.({ closable: true, - onCancel: close => mock(close), + onCancel: (close) => mock(close), }); await waitFakeTimer(); @@ -581,13 +627,13 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); }); - (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { + (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`press ESC to trigger ${type} onCancel`, async () => { const mock = jest.fn(); Modal[type]?.({ keyboard: true, - onCancel: close => mock(close), + onCancel: (close) => mock(close), }); await waitFakeTimer(); @@ -604,13 +650,13 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); }); - (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { + (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`click the mask to trigger ${type} onCancel`, async () => { const mock = jest.fn(); Modal[type]?.({ maskClosable: true, - onCancel: close => mock(close), + onCancel: (close) => mock(close), }); await waitFakeTimer(); @@ -632,7 +678,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { const mock = jest.fn(); Modal.confirm({ - onCancel: close => mock(close), + onCancel: (close) => mock(close), }); await waitFakeTimer(); @@ -645,7 +691,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { it('close can close modal when onCancel has a close parameter', async () => { Modal.confirm({ - onCancel: close => close(), + onCancel: (close) => close(), }); await waitFakeTimer();