diff --git a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx index 4f4e7c6a7e..b29f7051ca 100644 --- a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react'; +import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CombinedGraphQLErrors, @@ -44,7 +45,11 @@ export const PromiseRejectionEffect = () => { error?.networkError?.name === 'AbortError' || error?.name === 'AbortError'; - if (!isAbortError) { + const isViteStaleChunkLazyLoadingError = + error instanceof Error && + checkIfItsAViteStaleChunkLazyLoadingError(error); + + if (!isAbortError && !isViteStaleChunkLazyLoadingError) { enqueueErrorSnackBar( error instanceof Error ? { message: error.message } : {}, ); diff --git a/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx b/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx new file mode 100644 index 0000000000..a52ed4b321 --- /dev/null +++ b/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx @@ -0,0 +1,71 @@ +import { render, waitFor } from '@testing-library/react'; + +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; + +jest.mock('@sentry/react', () => ({ + captureException: jest.fn(), +})); + +const enqueueErrorSnackBar = jest.fn(); + +jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ + useSnackBar: () => ({ enqueueErrorSnackBar }), +})); + +const { captureException } = jest.requireMock('@sentry/react'); + +const dispatchUnhandledRejection = (reason: unknown) => { + const event = new Event('unhandledrejection'); + Object.defineProperty(event, 'reason', { value: reason }); + + window.dispatchEvent(event); +}; + +describe('PromiseRejectionEffect', () => { + beforeEach(() => { + jest.clearAllMocks(); + render(); + }); + + it('should not snackbar a stale chunk error', async () => { + dispatchUnhandledRejection(new Error('Importing a module script failed.')); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + + expect(enqueueErrorSnackBar).not.toHaveBeenCalled(); + }); + + it('should still snackbar an unrelated error', async () => { + dispatchUnhandledRejection(new Error('Some unrelated error')); + + expect(enqueueErrorSnackBar).toHaveBeenCalledWith({ + message: 'Some unrelated error', + }); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + }); + + it('should not snackbar an abort error', async () => { + dispatchUnhandledRejection({ name: 'AbortError' }); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + + expect(enqueueErrorSnackBar).not.toHaveBeenCalled(); + }); + + it('should snackbar a generic message when the reason is not an Error', async () => { + dispatchUnhandledRejection('something went wrong'); + + expect(enqueueErrorSnackBar).toHaveBeenCalledWith({}); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + }); +});