fix: Allow users to handle promise rejections in ActionButton's onOk callback (#40018)

* fix: Allow users to handle promise rejections in onOk callback

Components that rely on ActionButton swallow rejected promises. This makes it impossible for userland code to handle them.

* polish: Fix linting problems

* polish: Return rejected promise instead of throwing

* refact: Remove test for unhandled promise rejection

This test breaks when run in parallel by Jest. At the moment we have no way of changing the way Jest works and it prohibits us from testing for "unhandledRejection" events.

See: https://github.com/ant-design/ant-design/pull/40018#issuecomment-1373590259

* test: hack for rejection

Co-authored-by: 二货机器人 <smith3816@gmail.com>
This commit is contained in:
Peter Gassner 2023-01-07 17:27:33 +01:00 committed by GitHub
parent c35787696b
commit 25ea3a7202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 4 deletions

View File

@ -55,12 +55,10 @@ const ActionButton: React.FC<ActionButtonProps> = (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);
},
);
};

View File

@ -18,6 +18,45 @@ 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 <ActionButton {...props} actionFn={mockActionFn} />;
};
});
describe('Modal.confirm triggers callbacks correctly', () => {
// Inject CSSMotion to replace with No transition support
const MockCSSMotion = genCSSMotion(false);
@ -60,6 +99,11 @@ describe('Modal.confirm triggers callbacks correctly', () => {
jest.useFakeTimers();
});
beforeEach(() => {
(global as any).injectPromise = false;
(global as any).rejectPromise = null;
});
afterEach(async () => {
errorSpy.mockReset();
Modal.destroyAll();
@ -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 () => {