import { queryKey, sleep } from '@tanstack/query-test-utils'
import { fireEvent, render } from '@testing-library/preact'
import { useEffect, useState } from 'preact/hooks'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { MutationCache, QueryCache, QueryClient, useMutation } from '..'
import type { UseMutationResult } from '../types'
import { ErrorBoundary } from './ErrorBoundary'
import {
mockOnlineManagerIsOnline,
renderWithClient,
setActTimeout,
} from './utils'
describe('useMutation', () => {
let queryCache: QueryCache
let mutationCache: MutationCache
let queryClient: QueryClient
beforeEach(() => {
queryCache = new QueryCache()
mutationCache = new MutationCache()
queryClient = new QueryClient({
queryCache,
mutationCache,
})
vi.useFakeTimers()
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should be able to reset `data`', async () => {
function Page() {
const {
mutate,
data = 'empty',
reset,
} = useMutation({ mutationFn: () => Promise.resolve('mutation') })
return (
{data}
)
}
const { getByRole } = renderWithClient(queryClient, )
expect(getByRole('heading').textContent).toBe('empty')
fireEvent.click(getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(getByRole('heading').textContent).toBe('mutation')
fireEvent.click(getByRole('button', { name: /reset/i }))
await vi.advanceTimersByTimeAsync(0)
expect(getByRole('heading').textContent).toBe('empty')
})
it('should be able to reset `error`', async () => {
function Page() {
const { mutate, error, reset } = useMutation({
mutationFn: () => {
const err = new Error('Expected mock error. All is well!')
err.stack = ''
return Promise.reject(err)
},
})
return (
{error &&
{error.message}
}
)
}
const { getByRole, queryByRole } = renderWithClient(queryClient, )
expect(queryByRole('heading')).toBeNull()
fireEvent.click(getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(getByRole('heading').textContent).toBe(
'Expected mock error. All is well!',
)
fireEvent.click(getByRole('button', { name: /reset/i }))
await vi.advanceTimersByTimeAsync(0)
expect(queryByRole('heading')).toBeNull()
})
it('should call mutate callbacks when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSuccess', 'mutate.onSettled'])
})
it('should call mutateAsync callbacks when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
useEffect(() => {
setActTimeout(async () => {
await mutateAsync('todo', {
onSuccess: () => {
callbacks.push('mutateAsync.onSuccess')
},
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
},
})
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'mutateAsync.onSuccess',
'mutateAsync.onSettled',
])
})
it('should call mutate error callbacks when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onError', 'mutate.onSettled'])
})
it('should call mutateAsync error callbacks when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
})
useEffect(() => {
setActTimeout(async () => {
try {
await mutateAsync('todo', {
onError: () => {
callbacks.push('mutateAsync.onError')
},
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
},
})
} catch {}
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onError', 'mutateAsync.onSettled'])
})
it('should call only mutate onSuccess when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSuccess'])
})
it('should call only mutate onError when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onError'])
})
it('should call only mutate onSettled when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSettled'])
})
it('should call only mutateAsync onSuccess when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
useEffect(() => {
setActTimeout(async () => {
await mutateAsync('todo', {
onSuccess: () => {
callbacks.push('mutateAsync.onSuccess')
},
})
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSuccess'])
})
it('should call only mutateAsync onError when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
})
useEffect(() => {
setActTimeout(async () => {
try {
await mutateAsync('todo', {
onError: () => {
callbacks.push('mutateAsync.onError')
},
})
} catch {}
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onError'])
})
it('should call only mutateAsync onSettled when useMutation has no callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
})
useEffect(() => {
setActTimeout(async () => {
await mutateAsync('todo', {
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
},
})
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSettled'])
})
it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
let count = 0
const onSuccessMock = vi.fn()
const onSettledMock = vi.fn()
function Page() {
const { mutate } = useMutation({
mutationFn: (vars: { count: number }) => Promise.resolve(vars.count),
onSuccess: (data) => {
onSuccessMock(data)
},
onSettled: (data) => {
onSettledMock(data)
},
})
return (
{count}
)
}
const { getByRole } = renderWithClient(queryClient, )
expect(getByRole('heading').textContent).toBe('0')
fireEvent.click(getByRole('button', { name: /mutate/i }))
fireEvent.click(getByRole('button', { name: /mutate/i }))
fireEvent.click(getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(getByRole('heading').textContent).toBe('3')
expect(onSuccessMock).toHaveBeenCalledTimes(3)
expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1)
expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2)
expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3)
expect(onSettledMock).toHaveBeenCalledTimes(3)
expect(onSettledMock).toHaveBeenNthCalledWith(1, 1)
expect(onSettledMock).toHaveBeenNthCalledWith(2, 2)
expect(onSettledMock).toHaveBeenNthCalledWith(3, 3)
})
it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => {
let count = 0
type Value = { count: number }
const mutateFn = vi.fn<(value: Value) => Promise>()
mutateFn.mockImplementationOnce(() => {
return Promise.reject(new Error('Error test Jonas'))
})
mutateFn.mockImplementation((value) =>
sleep(10).then(() => Promise.resolve(value)),
)
function Page() {
const { mutate, failureCount, failureReason, data, status } = useMutation(
{ mutationFn: mutateFn },
)
return (
Data {data?.count}
Status {status}
Failed {failureCount} times
Failed because {failureReason?.message ?? 'null'}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('Data')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByText('Status error')).toBeInTheDocument()
expect(rendered.getByText('Failed 1 times')).toBeInTheDocument()
expect(
rendered.getByText('Failed because Error test Jonas'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByText('Status pending')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('Status success')).toBeInTheDocument()
expect(rendered.getByText('Data 2')).toBeInTheDocument()
expect(rendered.getByText('Failed 0 times')).toBeInTheDocument()
expect(rendered.getByText('Failed because null')).toBeInTheDocument()
})
it('should be able to call `onError` and `onSettled` after each failed mutate', async () => {
const onErrorMock = vi.fn()
const onSettledMock = vi.fn()
let count = 0
function Page() {
const { mutate } = useMutation({
mutationFn: (vars: { count: number }) => {
const error = new Error(
`Expected mock error. All is well! ${vars.count}`,
)
error.stack = ''
return Promise.reject(error)
},
onError: (error: Error) => {
onErrorMock(error.message)
},
onSettled: (_data, error) => {
onSettledMock(error?.message)
},
})
return (
{count}
)
}
const { getByRole } = renderWithClient(queryClient, )
expect(getByRole('heading').textContent).toBe('0')
fireEvent.click(getByRole('button', { name: /mutate/i }))
fireEvent.click(getByRole('button', { name: /mutate/i }))
fireEvent.click(getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(getByRole('heading').textContent).toBe('3')
expect(onErrorMock).toHaveBeenCalledTimes(3)
expect(onErrorMock).toHaveBeenNthCalledWith(
1,
'Expected mock error. All is well! 1',
)
expect(onErrorMock).toHaveBeenNthCalledWith(
2,
'Expected mock error. All is well! 2',
)
expect(onErrorMock).toHaveBeenNthCalledWith(
3,
'Expected mock error. All is well! 3',
)
expect(onSettledMock).toHaveBeenCalledTimes(3)
expect(onSettledMock).toHaveBeenNthCalledWith(
1,
'Expected mock error. All is well! 1',
)
expect(onSettledMock).toHaveBeenNthCalledWith(
2,
'Expected mock error. All is well! 2',
)
expect(onSettledMock).toHaveBeenNthCalledWith(
3,
'Expected mock error. All is well! 3',
)
})
it('should be able to call `onSuccess` callback after successful mutate', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess: () => {
callbacks.push('useMutation.onSuccess')
},
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['useMutation.onSuccess', 'mutate.onSuccess'])
})
it('should be able to call `onError` callback after failed mutate', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
onError: () => {
callbacks.push('useMutation.onError')
},
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['useMutation.onError', 'mutate.onError'])
})
it('should be able to call `onSettled` callback after mutate', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
onSettled: () => {
callbacks.push('useMutation.onSettled')
},
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['useMutation.onSettled', 'mutate.onSettled'])
})
it('should be able to call `onSuccess` callback after successful mutateAsync', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess: () => {
callbacks.push('useMutation.onSuccess')
},
})
useEffect(() => {
setActTimeout(async () => {
await mutateAsync('todo', {
onSuccess: () => {
callbacks.push('mutateAsync.onSuccess')
},
})
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSuccess',
'mutateAsync.onSuccess',
])
})
it('should be able to call `onError` callback after failed mutateAsync', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw new Error('oops')
}),
onError: () => {
callbacks.push('useMutation.onError')
},
})
useEffect(() => {
setActTimeout(async () => {
try {
await mutateAsync('todo', {
onError: () => {
callbacks.push('mutateAsync.onError')
},
})
} catch {}
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['useMutation.onError', 'mutateAsync.onError'])
})
it('should be able to call `onSettled` callback after mutateAsync', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
onSettled: () => {
callbacks.push('useMutation.onSettled')
},
})
useEffect(() => {
setActTimeout(async () => {
await mutateAsync('todo', {
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
},
})
}, 0)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSettled',
'mutateAsync.onSettled',
])
})
it('should be able to override the useMutation success callbacks', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (text: string) => Promise.resolve(text),
onSuccess: () => {
callbacks.push('useMutation.onSuccess')
return Promise.resolve()
},
onSettled: () => {
callbacks.push('useMutation.onSettled')
return Promise.resolve()
},
})
useEffect(() => {
setActTimeout(async () => {
try {
const result = await mutateAsync('todo', {
onSuccess: () => {
callbacks.push('mutateAsync.onSuccess')
return Promise.resolve()
},
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
return Promise.resolve()
},
})
callbacks.push(`mutateAsync.result:${result}`)
} catch {}
}, 10)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSuccess',
'useMutation.onSettled',
'mutateAsync.onSuccess',
'mutateAsync.onSettled',
'mutateAsync.result:todo',
])
})
it('should be able to override the error callbacks when using mutateAsync', async () => {
const callbacks: Array = []
function Page() {
const { mutateAsync } = useMutation({
mutationFn: (_text: string) => Promise.reject(new Error('oops')),
onError: () => {
callbacks.push('useMutation.onError')
return Promise.resolve()
},
onSettled: () => {
callbacks.push('useMutation.onSettled')
return Promise.resolve()
},
})
useEffect(() => {
setActTimeout(async () => {
try {
await mutateAsync('todo', {
onError: () => {
callbacks.push('mutateAsync.onError')
return Promise.resolve()
},
onSettled: () => {
callbacks.push('mutateAsync.onSettled')
return Promise.resolve()
},
})
} catch (error) {
callbacks.push(`mutateAsync.error:${(error as Error).message}`)
}
}, 10)
}, [mutateAsync])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onError',
'useMutation.onSettled',
'mutateAsync.onError',
'mutateAsync.onSettled',
'mutateAsync.error:oops',
])
})
it('should be able to override the error callbacks when using mutate', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
onError: () => {
callbacks.push('useMutation.onError')
},
onSettled: () => {
callbacks.push('useMutation.onSettled')
},
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onError',
'useMutation.onSettled',
'mutate.onError',
'mutate.onSettled',
])
})
it('should be able to override the settled callbacks when using mutate', async () => {
const callbacks: Array = []
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess: () => {
callbacks.push('useMutation.onSuccess')
},
onSettled: () => {
callbacks.push('useMutation.onSettled')
},
})
return (
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSuccess',
'useMutation.onSettled',
'mutate.onSuccess',
'mutate.onSettled',
])
})
it('should be able to use mutation defaults', async () => {
const key = queryKey()
queryClient.setMutationDefaults(key, {
mutationFn: (text: string) => sleep(10).then(() => text),
})
const states: Array> = []
function Page() {
const state = useMutation({ mutationKey: key })
states.push(state)
const { mutate } = state
useEffect(() => {
setActTimeout(() => {
mutate('todo')
}, 10)
}, [mutate])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(21)
expect(states.length).toBe(3)
expect(states[0]).toMatchObject({ data: undefined, isPending: false })
expect(states[1]).toMatchObject({ data: undefined, isPending: true })
expect(states[2]).toMatchObject({ data: 'todo', isPending: false })
})
it('should be able to retry a failed mutation', async () => {
let count = 0
function Page() {
const { mutate } = useMutation({
mutationFn: (_text: string) => {
count++
return Promise.reject(new Error('oops'))
},
retry: 1,
retryDelay: 5,
})
useEffect(() => {
setActTimeout(() => {
mutate('todo')
}, 10)
}, [mutate])
return null
}
renderWithClient(queryClient, )
await vi.advanceTimersByTimeAsync(15)
expect(count).toBe(2)
})
it('should not retry mutations while offline', async () => {
const onlineMock = mockOnlineManagerIsOnline(false)
let count = 0
function Page() {
const mutation = useMutation({
mutationFn: (_text: string) => {
count++
return Promise.reject(new Error('oops'))
},
retry: 1,
retryDelay: 5,
})
return (
error:{' '}
{mutation.error instanceof Error ? mutation.error.message : 'null'},
status: {mutation.status}, isPaused: {String(mutation.isPaused)}
)
}
const rendered = renderWithClient(queryClient, )
expect(
rendered.getByText('error: null, status: idle, isPaused: false'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(
rendered.getByText('error: null, status: pending, isPaused: true'),
).toBeInTheDocument()
expect(count).toBe(0)
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()
await vi.advanceTimersByTimeAsync(6)
expect(
rendered.getByText('error: oops, status: error, isPaused: false'),
).toBeInTheDocument()
expect(count).toBe(2)
onlineMock.mockRestore()
})
it('should call onMutate even if paused', async () => {
const onlineMock = mockOnlineManagerIsOnline(false)
const onMutate = vi.fn()
let count = 0
function Page() {
const mutation = useMutation({
mutationFn: async (_text: string) => {
count++
await sleep(10)
return count
},
onMutate,
})
return (
data: {mutation.data ?? 'null'}, status: {mutation.status},
isPaused: {String(mutation.isPaused)}
)
}
const rendered = renderWithClient(queryClient, )
expect(
rendered.getByText('data: null, status: idle, isPaused: false'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(
rendered.getByText('data: null, status: pending, isPaused: true'),
).toBeInTheDocument()
expect(onMutate).toHaveBeenCalledTimes(1)
expect(onMutate).toHaveBeenCalledWith('todo', {
client: queryClient,
meta: undefined,
mutationKey: undefined,
})
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('data: 1, status: success, isPaused: false'),
).toBeInTheDocument()
expect(onMutate).toHaveBeenCalledTimes(1)
expect(count).toBe(1)
onlineMock.mockRestore()
})
it('should optimistically go to paused state if offline', async () => {
const onlineMock = mockOnlineManagerIsOnline(false)
let count = 0
const states: Array = []
function Page() {
const mutation = useMutation({
mutationFn: async (_text: string) => {
count++
await sleep(10)
return count
},
})
states.push(`${mutation.status}, ${mutation.isPaused}`)
return (
data: {mutation.data ?? 'null'}, status: {mutation.status},
isPaused: {String(mutation.isPaused)}
)
}
const rendered = renderWithClient(queryClient, )
expect(
rendered.getByText('data: null, status: idle, isPaused: false'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(
rendered.getByText('data: null, status: pending, isPaused: true'),
).toBeInTheDocument()
// no intermediate 'pending, false' state is expected because we don't start mutating!
expect(states[0]).toBe('idle, false')
expect(states[1]).toBe('pending, true')
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('data: 1, status: success, isPaused: false'),
).toBeInTheDocument()
onlineMock.mockRestore()
})
it('should be able to retry a mutation when online', async () => {
const onlineMock = mockOnlineManagerIsOnline(false)
const key = queryKey()
let count = 0
function Page() {
const state = useMutation({
mutationKey: key,
mutationFn: (_text: string) =>
sleep(10).then(() => {
count++
return count > 1
? Promise.resolve(`data${count}`)
: Promise.reject(new Error('oops'))
}),
retry: 1,
retryDelay: 5,
networkMode: 'offlineFirst',
})
return (
status: {state.status}
isPaused: {String(state.isPaused)}
data: {state.data ?? 'null'}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('status: idle')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(16)
expect(rendered.getByText('isPaused: true')).toBeInTheDocument()
expect(
queryClient.getMutationCache().findAll({ mutationKey: key }).length,
).toBe(1)
expect(
queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state,
).toMatchObject({
status: 'pending',
isPaused: true,
failureCount: 1,
failureReason: new Error('oops'),
})
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data: data2')).toBeInTheDocument()
expect(
queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state,
).toMatchObject({
status: 'success',
isPaused: false,
failureCount: 0,
failureReason: null,
data: 'data2',
})
onlineMock.mockRestore()
})
// eslint-disable-next-line vitest/expect-expect
it('should not change state if unmounted', () => {
function Mutates() {
const { mutate } = useMutation({ mutationFn: () => sleep(10) })
return
}
function Page() {
const [mounted, setMounted] = useState(true)
return (
{mounted && }
)
}
const { getByText } = renderWithClient(queryClient, )
fireEvent.click(getByText('mutate'))
fireEvent.click(getByText('unmount'))
})
it('should be able to throw an error when throwOnError is set to true', async () => {
const err = new Error('Expected mock error. All is well!')
err.stack = ''
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const { mutate } = useMutation({
mutationFn: () => {
return Promise.reject(err)
},
throwOnError: true,
})
return (
)
}
const { getByText, queryByText } = renderWithClient(
queryClient,
(
error
)}
>
,
)
fireEvent.click(getByText('mutate'))
await vi.advanceTimersByTimeAsync(0)
expect(queryByText('error')).not.toBeNull()
expect(consoleMock.mock.calls[0]?.[1]).toBe(err)
consoleMock.mockRestore()
})
it('should be able to throw an error when throwOnError is a function that returns true', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
let boundary = false
function Page() {
const { mutate, error } = useMutation({
mutationFn: () => {
const err = new Error('mock error')
err.stack = ''
return Promise.reject(err)
},
throwOnError: () => {
return boundary
},
})
return (
{error && error.message}
)
}
const { getByText, queryByText } = renderWithClient(
queryClient,
(
error boundary
)}
>
,
)
// first error goes to component
fireEvent.click(getByText('mutate'))
await vi.advanceTimersByTimeAsync(0)
expect(queryByText('mock error')).not.toBeNull()
// second error goes to boundary
boundary = true
fireEvent.click(getByText('mutate'))
await vi.advanceTimersByTimeAsync(0)
expect(queryByText('error boundary')).not.toBeNull()
consoleMock.mockRestore()
})
it('should not throw an error when throwOnError is set to false', async () => {
function Page() {
const { mutate, error } = useMutation({
mutationFn: () =>
sleep(10).then(() => {
throw new Error('Expected mock error')
}),
throwOnError: false,
})
return (
error: {error?.message ?? 'null'}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('error: Expected mock error')).toBeInTheDocument()
})
it('should not throw an error when throwOnError is a function that returns false', async () => {
function Page() {
const { mutate, error } = useMutation({
mutationFn: () =>
sleep(10).then(() => {
throw new Error('Expected mock error')
}),
throwOnError: () => false,
})
return (
error: {error?.message ?? 'null'}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('error: Expected mock error')).toBeInTheDocument()
})
it('should not throw an error when throwOnError is not set', async () => {
function Page() {
const { mutate, error } = useMutation({
mutationFn: () =>
sleep(10).then(() => {
throw new Error('Expected mock error')
}),
})
return (
error: {error?.message ?? 'null'}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('error: Expected mock error')).toBeInTheDocument()
})
it('should pass meta to mutation on success', async () => {
const successMock = vi.fn()
const queryClientMutationMeta = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_, __, ___, mutation) => {
successMock(mutation.meta?.metaSuccessMessage)
},
}),
})
const metaSuccessMessage = 'mutation succeeded'
function Page() {
const { mutate: succeed, isSuccess } = useMutation({
mutationFn: () => Promise.resolve(''),
meta: { metaSuccessMessage },
})
return (
{isSuccess &&
successTest
}
)
}
const { getByText, queryByText } = renderWithClient(
queryClientMutationMeta,
,
)
fireEvent.click(getByText('succeed'))
await vi.advanceTimersByTimeAsync(0)
expect(queryByText('successTest')).not.toBeNull()
expect(successMock).toHaveBeenCalledTimes(1)
expect(successMock).toHaveBeenCalledWith(metaSuccessMessage)
})
it('should pass meta to mutation on error', async () => {
const errorMock = vi.fn()
const queryClientMutationMeta = new QueryClient({
mutationCache: new MutationCache({
onError: (_, __, ___, mutation) => {
errorMock(mutation.meta?.metaErrorMessage)
},
}),
})
const metaErrorMessage = 'mutation failed'
function Page() {
const { mutate: error, isError } = useMutation({
mutationFn: () => {
return Promise.reject(new Error(''))
},
meta: { metaErrorMessage },
})
return (
{isError &&
errorTest
}
)
}
const { getByText, queryByText } = renderWithClient(
queryClientMutationMeta,
,
)
fireEvent.click(getByText('error'))
await vi.advanceTimersByTimeAsync(0)
expect(queryByText('errorTest')).not.toBeNull()
expect(errorMock).toHaveBeenCalledTimes(1)
expect(errorMock).toHaveBeenCalledWith(metaErrorMessage)
})
it('should call cache callbacks when unmounted', async () => {
const onSuccess = vi.fn()
const onSuccessMutate = vi.fn()
const onSettled = vi.fn()
const onSettledMutate = vi.fn()
const mutationKey = queryKey()
let count = 0
function Page() {
const [show, setShow] = useState(true)
return (
{show && }
)
}
function Component() {
const mutation = useMutation({
mutationFn: async (_text: string) => {
count++
await sleep(10)
return count
},
mutationKey,
gcTime: 0,
onSuccess,
onSettled,
})
return (
data: {mutation.data ?? 'null'}, status: {mutation.status},
isPaused: {String(mutation.isPaused)}
)
}
const rendered = renderWithClient(queryClient, )
expect(
rendered.getByText('data: null, status: idle, isPaused: false'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
fireEvent.click(rendered.getByRole('button', { name: /hide/i }))
await vi.advanceTimersByTimeAsync(10)
expect(
queryClient.getMutationCache().findAll({ mutationKey }),
).toHaveLength(0)
expect(count).toBe(1)
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSuccessMutate).toHaveBeenCalledTimes(0)
expect(onSettledMutate).toHaveBeenCalledTimes(0)
})
it('should call mutate callbacks only for the last observer', async () => {
const onSuccess = vi.fn()
const onSuccessMutate = vi.fn()
const onSettled = vi.fn()
const onSettledMutate = vi.fn()
let count = 0
function Page() {
const mutation = useMutation({
mutationFn: async (text: string) => {
count++
const result = `result-${text}`
await sleep(10)
return result
},
onSuccess,
onSettled,
})
return (
data: {mutation.data ?? 'null'}, status: {mutation.status}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('data: result-todo2, status: success'),
).toBeInTheDocument()
expect(count).toBe(2)
expect(onSuccess).toHaveBeenCalledTimes(2)
expect(onSuccess).toHaveBeenNthCalledWith(
1,
'result-todo1',
'todo1',
undefined,
{
client: queryClient,
meta: undefined,
mutationKey: undefined,
},
)
expect(onSuccess).toHaveBeenNthCalledWith(
2,
'result-todo2',
'todo2',
undefined,
{
client: queryClient,
meta: undefined,
mutationKey: undefined,
},
)
expect(onSettled).toHaveBeenCalledTimes(2)
expect(onSuccessMutate).toHaveBeenCalledTimes(1)
expect(onSuccessMutate).toHaveBeenCalledWith(
'result-todo2',
'todo2',
undefined,
{
client: queryClient,
meta: undefined,
mutationKey: undefined,
},
)
expect(onSettledMutate).toHaveBeenCalledTimes(1)
expect(onSettledMutate).toHaveBeenCalledWith(
'result-todo2',
null,
'todo2',
undefined,
{
client: queryClient,
meta: undefined,
mutationKey: undefined,
},
)
})
it('should go to error state if onSuccess callback errors', async () => {
const error = new Error('error from onSuccess')
const onError = vi.fn()
function Page() {
const mutation = useMutation({
mutationFn: (_text: string) => sleep(10).then(() => 'result'),
onSuccess: () => Promise.reject(error),
onError,
})
return (
status: {mutation.status}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('status: idle')).toBeInTheDocument()
rendered.getByRole('button', { name: /mutate/i }).click()
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('status: error')).toBeInTheDocument()
expect(onError).toHaveBeenCalledWith(error, 'todo', undefined, {
client: queryClient,
meta: undefined,
mutationKey: undefined,
})
})
it('should go to error state if onError callback errors', async ({
onTestFinished,
}) => {
const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionFn)
})
const error = new Error('error from onError')
const mutateFnError = new Error('mutateFnError')
function Page() {
const mutation = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw mutateFnError
}),
onError: () => Promise.reject(error),
})
return (
error:{' '}
{mutation.error instanceof Error ? mutation.error.message : 'null'},
status: {mutation.status}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument()
rendered.getByRole('button', { name: /mutate/i }).click()
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('error: mutateFnError, status: error'),
).toBeInTheDocument()
})
it('should go to error state if onSettled callback errors', async ({
onTestFinished,
}) => {
const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionFn)
})
const error = new Error('error from onSettled')
const mutateFnError = new Error('mutateFnError')
const onError = vi.fn()
function Page() {
const mutation = useMutation({
mutationFn: (_text: string) =>
sleep(10).then(() => {
throw mutateFnError
}),
onSettled: () => Promise.reject(error),
onError,
})
return (
error:{' '}
{mutation.error instanceof Error ? mutation.error.message : 'null'},
status: {mutation.status}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument()
rendered.getByRole('button', { name: /mutate/i }).click()
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('error: mutateFnError, status: error'),
).toBeInTheDocument()
expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined, {
client: queryClient,
meta: undefined,
mutationKey: undefined,
})
})
it('should use provided custom queryClient', async () => {
function Page() {
const mutation = useMutation(
{
mutationFn: (text: string) => {
return Promise.resolve(text)
},
},
queryClient,
)
return (
data: {mutation.data ?? 'null'}, status: {mutation.status}
)
}
const rendered = render()
expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(
rendered.getByText('data: custom client, status: success'),
).toBeInTheDocument()
})
it('should be able to chain mutateAsync calls sequentially', async () => {
function Page() {
const [result, setResult] = useState('idle')
const { mutateAsync: createUserAsync } = useMutation({
mutationFn: (name: string) => sleep(10).then(() => ({ id: '1', name })),
})
const { mutateAsync: updateProfileAsync } = useMutation({
mutationFn: (userId: string) =>
sleep(10).then(() => `profile updated for ${userId}`),
})
return (
result: {result}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /chain/i }))
await vi.advanceTimersByTimeAsync(10)
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('result: profile updated for 1'),
).toBeInTheDocument()
})
it('should handle error in chained mutateAsync calls', async () => {
function Page() {
const [result, setResult] = useState('idle')
const { mutateAsync: createUserAsync } = useMutation({
mutationFn: (_name: string) =>
sleep(10).then<{ id: string }>(() => {
throw new Error('create failed')
}),
})
const { mutateAsync: updateProfileAsync } = useMutation({
mutationFn: (userId: string) =>
sleep(10).then(() => `profile updated for ${userId}`),
})
return (
result: {result}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /chain/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('result: error: create failed'),
).toBeInTheDocument()
})
it('should set success message from mutate callbacks when mutation succeeds', async () => {
function Page() {
const [message, setMessage] = useState('idle')
const { mutate } = useMutation({
mutationFn: (shouldFail: boolean) =>
sleep(10).then(() => {
if (shouldFail) {
throw new Error('submission failed')
}
return 'submitted successfully'
}),
retry: false,
})
return (
message: {message}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /^submit$/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('message: success: submitted successfully'),
).toBeInTheDocument()
})
it('should set error message from mutate callbacks when mutation fails', async () => {
function Page() {
const [message, setMessage] = useState('idle')
const { mutate } = useMutation({
mutationFn: (shouldFail: boolean) =>
sleep(10).then(() => {
if (shouldFail) {
throw new Error('submission failed')
}
return 'submitted successfully'
}),
retry: false,
})
return (
message: {message}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /submit fail/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('message: error: submission failed'),
).toBeInTheDocument()
})
it('should handle conditional error with retry using mutate', async () => {
let attempt = 0
function Page() {
const [message, setMessage] = useState('idle')
const { mutate } = useMutation({
mutationFn: () =>
sleep(10).then(() => {
attempt++
if (attempt < 2) {
throw new Error('temporary failure')
}
return 'success'
}),
retry: false,
})
return (
message: {message}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /submit/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('message: failed, retrying...'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /submit/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('message: result: success')).toBeInTheDocument()
})
it('should support optimistic update on success', async () => {
function Page() {
const [items, setItems] = useState>([
'item1',
'item2',
'item3',
])
const [successMessage, setSuccessMessage] = useState('')
const { mutate } = useMutation({
mutationFn: (item: string) => sleep(10).then(() => item),
onMutate: (item) => {
const previousItems = [...items]
setItems((prev) => prev.filter((i) => i !== item))
return { previousItems }
},
onSuccess: (deletedItem) => {
setSuccessMessage(`deleted: ${deletedItem}`)
},
onError: (_error, _item, context) => {
if (context?.previousItems) {
setItems(context.previousItems)
}
},
})
return (
{items.map((item) => (
))}
items: {items.join(', ')}
success: {successMessage || 'none'}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('items: item1, item2, item3')).toBeInTheDocument()
expect(rendered.getByText('success: none')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /delete item2/i }))
// optimistic update: item2 removed immediately
expect(rendered.getByText('items: item1, item3')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(11)
// success: item2 stays removed and onSuccess called
expect(rendered.getByText('items: item1, item3')).toBeInTheDocument()
expect(rendered.getByText('success: deleted: item2')).toBeInTheDocument()
})
it('should support optimistic update and rollback on error', async () => {
function Page() {
const [items, setItems] = useState>([
'item1',
'item2',
'item3',
])
const [message, setMessage] = useState('')
const { mutate } = useMutation({
mutationFn: (item: string) =>
sleep(10).then(() => {
throw new Error(`Failed to delete ${item}`)
}),
onMutate: (item) => {
const previousItems = [...items]
setItems((prev) => prev.filter((i) => i !== item))
return { previousItems }
},
onSuccess: (deletedItem) => {
setMessage(`deleted: ${deletedItem}`)
},
onError: (_error, _item, context) => {
setMessage('rollback')
if (context?.previousItems) {
setItems(context.previousItems)
}
},
retry: false,
})
return (
{items.map((item) => (
))}
items: {items.join(', ')}
message: {message || 'none'}
)
}
const rendered = renderWithClient(queryClient, )
expect(rendered.getByText('items: item1, item2, item3')).toBeInTheDocument()
expect(rendered.getByText('message: none')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /delete item2/i }))
// optimistic update: item2 removed immediately
expect(rendered.getByText('items: item1, item3')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(11)
// rollback: item2 restored after error, onSuccess not called
expect(rendered.getByText('items: item1, item2, item3')).toBeInTheDocument()
expect(rendered.getByText('message: rollback')).toBeInTheDocument()
})
it('should update the cache in onMutate and roll back via onMutateResult in onError', async () => {
const key = queryKey()
queryClient.setQueryData>(key, ['Todo 1'])
function Page() {
const { mutate } = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
onMutate: async (newTodo: string) => {
await queryClient.cancelQueries({ queryKey: key })
const previousTodos = queryClient.getQueryData>(key)
queryClient.setQueryData>(key, (old) => [
...(old ?? []),
newTodo,
])
return { previousTodos }
},
onError: (_err, _newTodo, onMutateResult) => {
queryClient.setQueryData(key, onMutateResult?.previousTodos)
},
})
return
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
// The optimistic value lands after onMutate's first await, so flush
// microtasks before asserting.
await vi.advanceTimersByTimeAsync(0)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
await vi.advanceTimersByTimeAsync(11)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1'])
})
it('should keep the optimistic update in place when the mutation succeeds', async () => {
const key = queryKey()
queryClient.setQueryData>(key, ['Todo 1'])
function Page() {
const { mutate } = useMutation({
mutationFn: (newTodo: string) => sleep(10).then(() => newTodo),
onMutate: async (newTodo: string) => {
await queryClient.cancelQueries({ queryKey: key })
const previousTodos = queryClient.getQueryData>(key)
queryClient.setQueryData>(key, (old) => [
...(old ?? []),
newTodo,
])
return { previousTodos }
},
})
return
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
await vi.advanceTimersByTimeAsync(11)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
})
it('should be able to run multiple mutateAsync calls in parallel with Promise.all', async () => {
function Page() {
const [result, setResult] = useState('idle')
const { mutateAsync } = useMutation({
mutationFn: (file: string) => sleep(10).then(() => `uploaded: ${file}`),
})
return (
result: {result}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText(
'result: uploaded: file1, uploaded: file2, uploaded: file3',
),
).toBeInTheDocument()
})
it('should handle Promise.all rejection when one parallel mutateAsync call fails', async () => {
function Page() {
const [result, setResult] = useState('idle')
const { mutateAsync } = useMutation({
mutationFn: (file: string) =>
sleep(10).then(() => {
if (file === 'file2') {
throw new Error('upload failed')
}
return `uploaded: ${file}`
}),
retry: false,
})
return (
result: {result}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('result: error: upload failed'),
).toBeInTheDocument()
})
it('should handle partial failure in parallel mutateAsync calls with Promise.allSettled', async () => {
function Page() {
const [result, setResult] = useState('idle')
const { mutateAsync } = useMutation({
mutationFn: (file: string) =>
sleep(10).then(() => {
if (file === 'file2') {
throw new Error('upload failed')
}
return `uploaded: ${file}`
}),
retry: false,
})
return (
result: {result}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText(
'result: uploaded: file1, error: upload failed, uploaded: file3',
),
).toBeInTheDocument()
})
it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => {
const onSuccess = vi.fn()
function Page() {
const { mutate } = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text.toUpperCase()),
onMutate: (text: string) => ({ startedWith: text }),
onSuccess,
})
return
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(onSuccess).toHaveBeenCalledTimes(1)
const [data, variables, onMutateResult, context] = onSuccess.mock.calls[0]!
expect(data).toBe('TODO')
expect(variables).toBe('todo')
expect(onMutateResult).toEqual({ startedWith: 'todo' })
expect(context.client).toBe(queryClient)
expect(context.meta).toBeUndefined()
expect(context.mutationKey).toBeUndefined()
})
it('should give mutationFn the same QueryClient instance via context', async () => {
const key = queryKey()
queryClient.setQueryData(key, 'tag-from-this-client')
function Page() {
const { mutate, data } = useMutation({
mutationFn: (_text: string, context) =>
sleep(10).then(() => context.client.getQueryData(key)),
})
return (
data: {String(data)}
)
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data: tag-from-this-client')).toBeInTheDocument()
})
it('should include mutationKey in the context passed to hook-level callbacks', async () => {
const onSuccess = vi.fn()
function Page() {
const { mutate } = useMutation({
mutationKey: ['todos', 'add'],
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess,
})
return
}
const rendered = renderWithClient(queryClient, )
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onSuccess.mock.calls[0]?.[3].mutationKey).toEqual(['todos', 'add'])
})
it('should let onSuccess invalidate queries via context.client without a useQueryClient() closure', async () => {
const key = queryKey()
queryClient.setQueryData(key, 'data')
function Page() {
const { mutate } = useMutation({
mutationFn: () => sleep(10).then(() => 'mutated'),
onSuccess: (_data, _variables, _onMutateResult, context) => {
context.client.invalidateQueries({ queryKey: key })
},
})
return
}
const rendered = renderWithClient(queryClient, )
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(false)
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true)
})
})