diff --git a/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx b/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx new file mode 100644 index 0000000..7b07073 --- /dev/null +++ b/client/src/shared/lib/__tests__/use-mutation-with-toast.test.tsx @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, waitFor } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useMutationWithToast } from '../use-mutation-with-toast' +import { addNotification } from '../../model/notification' + +vi.mock('../../model/notification', () => ({ + addNotification: vi.fn(), +})) + +function createWrapper() { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + return ({ children }: { children: React.ReactNode }) => ( + {children} + ) +} + +describe('useMutationWithToast', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('shows success notification on success with successMessage', async () => { + const mutationFn = vi.fn().mockResolvedValue({ ok: true }) + const { result } = renderHook(() => useMutationWithToast({ mutationFn, successMessage: 'Done!' }), { + wrapper: createWrapper(), + }) + result.current.mutate() + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(addNotification).toHaveBeenCalledWith({ type: 'success', message: 'Done!' }) + }) + + it('does NOT show success notification without successMessage', async () => { + const mutationFn = vi.fn().mockResolvedValue({ ok: true }) + const { result } = renderHook(() => useMutationWithToast({ mutationFn }), { wrapper: createWrapper() }) + result.current.mutate() + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(addNotification).not.toHaveBeenCalled() + }) + + it('shows error notification on mutation error', async () => { + const mutationFn = vi.fn().mockRejectedValue(new Error('Boom')) + const { result } = renderHook(() => useMutationWithToast({ mutationFn }), { wrapper: createWrapper() }) + result.current.mutate() + await waitFor(() => expect(result.current.isError).toBe(true)) + expect(addNotification).toHaveBeenCalledWith({ type: 'error', message: 'Boom' }) + }) + + it('calls user-provided onSuccess callback', async () => { + const onSuccess = vi.fn() + const mutationFn = vi.fn().mockResolvedValue({ ok: true }) + const { result } = renderHook(() => useMutationWithToast({ mutationFn, onSuccess, successMessage: 'OK' }), { + wrapper: createWrapper(), + }) + result.current.mutate() + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(onSuccess).toHaveBeenCalled() + }) + + it('calls user-provided onError callback', async () => { + const onError = vi.fn() + const mutationFn = vi.fn().mockRejectedValue(new Error('fail')) + const { result } = renderHook(() => useMutationWithToast({ mutationFn, onError }), { wrapper: createWrapper() }) + result.current.mutate() + await waitFor(() => expect(result.current.isError).toBe(true)) + expect(onError).toHaveBeenCalled() + }) +}) diff --git a/client/src/shared/lib/use-mutation-with-toast.ts b/client/src/shared/lib/use-mutation-with-toast.ts new file mode 100644 index 0000000..e2a3e04 --- /dev/null +++ b/client/src/shared/lib/use-mutation-with-toast.ts @@ -0,0 +1,32 @@ +import { useMutation, type UseMutationOptions } from '@tanstack/react-query' +import { addNotification } from '../model/notification' +import { getApiErrorMessage } from './get-api-error-message' + +type MutationWithToastOptions = UseMutationOptions< + TData, + TError, + TVariables, + TContext +> & { + successMessage?: string +} + +export function useMutationWithToast( + options: MutationWithToastOptions, +) { + const { successMessage, onSuccess, onError, ...mutationOptions } = options + + return useMutation({ + ...mutationOptions, + onSuccess: (data, variables, context) => { + if (successMessage) { + addNotification({ type: 'success', message: successMessage }) + } + onSuccess?.(data, variables, context) + }, + onError: (error, variables, context) => { + addNotification({ type: 'error', message: getApiErrorMessage(error) }) + onError?.(error, variables, context) + }, + }) +}