snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,771 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { reactive, ref } from 'vue-demi'
import { noop } from '@tanstack/query-core'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { useMutation } from '../useMutation'
import { useQueryClient } from '../useQueryClient'
import type { MutationFunctionContext } from '@tanstack/query-core'
vi.mock('../useQueryClient')
describe('useMutation', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should be in idle state initially', () => {
const mutation = useMutation({
mutationFn: (params) => sleep(0).then(() => params),
})
expect(mutation).toMatchObject({
isIdle: { value: true },
isPending: { value: false },
isError: { value: false },
isSuccess: { value: false },
})
})
it('should change state after invoking mutate', () => {
const result = 'Mock data'
const mutation = useMutation({
mutationFn: (params: string) => sleep(0).then(() => params),
})
mutation.mutate(result)
expect(mutation).toMatchObject({
isIdle: { value: false },
isPending: { value: true },
isError: { value: false },
isSuccess: { value: false },
data: { value: undefined },
error: { value: null },
})
})
it('should return error when request fails', async () => {
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation.mutate()
await vi.advanceTimersByTimeAsync(10)
expect(mutation).toMatchObject({
isIdle: { value: false },
isPending: { value: false },
isError: { value: true },
isSuccess: { value: false },
data: { value: undefined },
error: { value: Error('Some error') },
})
})
it('should return data when request succeeds', async () => {
const result = 'Mock data'
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutate(result)
await vi.advanceTimersByTimeAsync(10)
expect(mutation).toMatchObject({
isIdle: { value: false },
isPending: { value: false },
isError: { value: false },
isSuccess: { value: true },
data: { value: 'Mock data' },
error: { value: null },
})
})
it('should work with options getter and be reactive', async () => {
const key = queryKey()
const result = 'Mock data'
const keyRef = ref('key01')
const fnMock = vi.fn((params: string) => sleep(10).then(() => params))
const mutation = useMutation(() => ({
mutationKey: [...key, keyRef.value],
mutationFn: fnMock,
}))
mutation.mutate(result)
await vi.advanceTimersByTimeAsync(10)
expect(fnMock).toHaveBeenCalledTimes(1)
expect(fnMock).toHaveBeenNthCalledWith(
1,
result,
expect.objectContaining({ mutationKey: [...key, 'key01'] }),
)
keyRef.value = 'key02'
await vi.advanceTimersByTimeAsync(0)
mutation.mutate(result)
await vi.advanceTimersByTimeAsync(10)
expect(fnMock).toHaveBeenCalledTimes(2)
expect(fnMock).toHaveBeenNthCalledWith(
2,
result,
expect.objectContaining({ mutationKey: [...key, 'key02'] }),
)
})
it('should update reactive options', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryClient = useQueryClient()
const mutationCache = queryClient.getMutationCache()
const options = reactive({
mutationKey: key1,
mutationFn: (params: string) => sleep(10).then(() => params),
})
const mutation = useMutation(options)
options.mutationKey = key2
await vi.advanceTimersByTimeAsync(10)
mutation.mutate('xyz')
await vi.advanceTimersByTimeAsync(10)
const mutations = mutationCache.find({ mutationKey: key2 })
expect(mutations?.options.mutationKey).toEqual(key2)
})
it('should update reactive options deeply', async () => {
type MutationKeyTest = {
entity: string
otherObject: {
name: string
}
}
const mutationKey = ref<Array<MutationKeyTest>>([
{
entity: 'test',
otherObject: { name: 'objectName' },
},
])
const queryClient = useQueryClient()
const mutationCache = queryClient.getMutationCache()
const options = reactive({
mutationKey,
mutationFn: (params: string) => sleep(10).then(() => params),
})
const mutation = useMutation(options)
mutationKey.value[0]!.otherObject.name = 'someOtherObjectName'
await vi.advanceTimersByTimeAsync(10)
mutation.mutate('xyz')
await vi.advanceTimersByTimeAsync(10)
const mutations = mutationCache.getAll()
const relevantMutation = mutations.find((m) => {
return (
Array.isArray(m.options.mutationKey) &&
!!m.options.mutationKey[0].otherObject
)
})
expect(
(relevantMutation?.options.mutationKey as Array<MutationKeyTest>)[0]
?.otherObject.name === 'someOtherObjectName',
).toBe(true)
})
it('should allow for non-options object (mutationFn or mutationKey) passed as arg1 & arg2 to trigger reactive updates', async () => {
const key1 = queryKey()
const key2 = queryKey()
const mutationKeyRef = ref<Array<string>>(key1)
const mutationFn = ref((params: string) => sleep(0).then(() => params))
const queryClient = useQueryClient()
const mutationCache = queryClient.getMutationCache()
const mutation = useMutation({ mutationKey: mutationKeyRef, mutationFn })
mutationKeyRef.value = key2
let proof = false
mutationFn.value = (params: string) => {
proof = true
return sleep(10).then(() => params)
}
await vi.advanceTimersByTimeAsync(10)
mutation.mutate('xyz')
await vi.advanceTimersByTimeAsync(10)
const mutations = mutationCache.find({ mutationKey: key2 })
expect(mutations?.options.mutationKey).toEqual(key2)
expect(proof).toEqual(true)
})
it('should reset state after invoking mutation.reset', async () => {
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation.mutate()
await vi.advanceTimersByTimeAsync(10)
mutation.reset()
expect(mutation).toMatchObject({
isIdle: { value: true },
isPending: { value: false },
isError: { value: false },
isSuccess: { value: false },
data: { value: undefined },
error: { value: null },
})
})
describe('side effects', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should call onMutate when passed as an option', async () => {
const onMutate = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
onMutate,
})
mutation.mutate('')
await vi.advanceTimersByTimeAsync(10)
expect(onMutate).toHaveBeenCalledTimes(1)
})
it('should call onError when passed as an option', async () => {
const onError = vi.fn()
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
onError,
})
mutation.mutate('')
await vi.advanceTimersByTimeAsync(10)
expect(onError).toHaveBeenCalledTimes(1)
})
it('should call onSuccess when passed as an option', async () => {
const onSuccess = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
onSuccess,
})
mutation.mutate('')
await vi.advanceTimersByTimeAsync(10)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
it('should call onSettled when passed as an option', async () => {
const onSettled = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
onSettled,
})
mutation.mutate('')
await vi.advanceTimersByTimeAsync(10)
expect(onSettled).toHaveBeenCalledTimes(1)
})
it('should call onError when passed as an argument of mutate function', async () => {
const onError = vi.fn()
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation.mutate(undefined, { onError })
await vi.advanceTimersByTimeAsync(10)
expect(onError).toHaveBeenCalledTimes(1)
})
it('should call onSuccess when passed as an argument of mutate function', async () => {
const onSuccess = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutate('', { onSuccess })
await vi.advanceTimersByTimeAsync(10)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
it('should call onSettled when passed as an argument of mutate function', async () => {
const onSettled = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutate('', { onSettled })
await vi.advanceTimersByTimeAsync(10)
expect(onSettled).toHaveBeenCalledTimes(1)
})
it('should call onSuccess and onSettled when passed as arguments of mutate function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutate('', {
onSuccess: () => callbacks.push('mutate.onSuccess'),
onSettled: () => callbacks.push('mutate.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSuccess', 'mutate.onSettled'])
})
it('should call onError and onSettled when passed as arguments of mutate function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (_params: string) =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation.mutate('', {
onError: () => callbacks.push('mutate.onError'),
onSettled: () => callbacks.push('mutate.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onError', 'mutate.onSettled'])
})
it('should call onSuccess and onSettled when passed as arguments of mutateAsync function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutateAsync('', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'mutateAsync.onSuccess',
'mutateAsync.onSettled',
])
})
it('should call onError and onSettled when passed as arguments of mutateAsync function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (_params: string) =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation
.mutateAsync('', {
onError: () => callbacks.push('mutateAsync.onError'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
.catch(noop)
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'mutateAsync.onError',
'mutateAsync.onSettled',
])
})
it('should call onSuccess when passed as an argument of mutateAsync function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutateAsync('', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSuccess'])
})
it('should call onError when passed as an argument of mutateAsync function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (_params: string) =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
mutation
.mutateAsync('', {
onError: () => callbacks.push('mutateAsync.onError'),
})
.catch(noop)
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onError'])
})
it('should call onSettled when passed as an argument of mutateAsync function', async () => {
const callbacks: Array<string> = []
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
mutation.mutateAsync('', {
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSettled'])
})
it('should fire both onSettled functions', async () => {
const onSettled = vi.fn()
const onSettledOnFunction = vi.fn()
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
onSettled,
})
mutation.mutate('', { onSettled: onSettledOnFunction })
await vi.advanceTimersByTimeAsync(10)
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettledOnFunction).toHaveBeenCalledTimes(1)
})
})
describe('async', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should resolve properly', async () => {
const result = 'Mock data'
const mutation = useMutation({
mutationFn: (params: string) => sleep(10).then(() => params),
})
const promise = mutation.mutateAsync(result)
await vi.advanceTimersByTimeAsync(10)
await expect(promise).resolves.toBe(result)
expect(mutation).toMatchObject({
isIdle: { value: false },
isPending: { value: false },
isError: { value: false },
isSuccess: { value: true },
data: { value: 'Mock data' },
error: { value: null },
})
})
it('should throw on error', async () => {
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
})
await Promise.all([
expect(mutation.mutateAsync()).rejects.toThrow('Some error'),
vi.advanceTimersByTimeAsync(10),
])
expect(mutation).toMatchObject({
isIdle: { value: false },
isPending: { value: false },
isError: { value: true },
isSuccess: { value: false },
data: { value: undefined },
error: { value: Error('Some error') },
})
})
})
it('should warn when used outside of setup function in development mode', () => {
vi.stubEnv('NODE_ENV', 'development')
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
useMutation({
mutationFn: (params: string) => sleep(0).then(() => params),
})
expect(warnSpy).toHaveBeenCalledWith(
'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
)
} finally {
warnSpy.mockRestore()
vi.unstubAllEnvs()
}
})
describe('throwOnError', () => {
it('should evaluate throwOnError when mutation is expected to throw', async () => {
const err = new Error('Expected mock error. All is well!')
const boundaryFn = vi.fn()
const { mutate } = useMutation({
mutationFn: () => sleep(10).then(() => Promise.reject(err)),
throwOnError: boundaryFn,
})
mutate()
await vi.advanceTimersByTimeAsync(10)
expect(boundaryFn).toHaveBeenCalledTimes(1)
expect(boundaryFn).toHaveBeenCalledWith(err)
})
it('should throw from error watcher when throwOnError returns true', async () => {
const throwOnErrorFn = vi.fn().mockReturnValue(true)
const { mutate } = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
throwOnError: throwOnErrorFn,
})
mutate()
// Suppress the Unhandled Rejection caused by watcher throw in Vue 3
const rejectionHandler = () => {}
process.on('unhandledRejection', rejectionHandler)
await vi.advanceTimersByTimeAsync(10)
process.off('unhandledRejection', rejectionHandler)
expect(throwOnErrorFn).toHaveBeenCalledTimes(1)
expect(throwOnErrorFn).toHaveBeenCalledWith(Error('Some error'))
})
})
describe('optimistic updates', () => {
it('should update the cache in onMutate and roll back via onMutateResult in onError', async () => {
const key = queryKey()
const queryClient = useQueryClient()
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
const mutation = useMutation({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
onMutate: async (newTodo: string) => {
await queryClient.cancelQueries({ queryKey: key })
const previousTodos = queryClient.getQueryData<Array<string>>(key)
queryClient.setQueryData<Array<string>>(key, (old) => [
...(old ?? []),
newTodo,
])
return { previousTodos }
},
onError: (_err, _newTodo, onMutateResult) => {
queryClient.setQueryData(key, onMutateResult?.previousTodos)
},
})
mutation.mutate('Todo 2')
// onMutate runs synchronously up to its first await, so the optimistic
// value is visible immediately, before the mutationFn settles.
await vi.advanceTimersByTimeAsync(0)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
await vi.advanceTimersByTimeAsync(10)
expect(mutation).toMatchObject({ isError: { value: true } })
expect(queryClient.getQueryData(key)).toEqual(['Todo 1'])
})
it('should keep the optimistic update in place when the mutation succeeds', async () => {
const key = queryKey()
const queryClient = useQueryClient()
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
const mutation = useMutation({
mutationFn: (newTodo: string) => sleep(10).then(() => newTodo),
onMutate: async (newTodo: string) => {
await queryClient.cancelQueries({ queryKey: key })
const previousTodos = queryClient.getQueryData<Array<string>>(key)
queryClient.setQueryData<Array<string>>(key, (old) => [
...(old ?? []),
newTodo,
])
return { previousTodos }
},
onError: (_err, _newTodo, onMutateResult) => {
queryClient.setQueryData(key, onMutateResult?.previousTodos)
},
})
mutation.mutate('Todo 2')
await vi.advanceTimersByTimeAsync(10)
expect(mutation).toMatchObject({ isSuccess: { value: true } })
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
})
})
describe('concurrent mutate calls', () => {
it('should report each call result independently when some fail', async () => {
const mutation = useMutation({
mutationFn: (todo: string) =>
todo === 'bad'
? sleep(10).then(() => Promise.reject(new Error('Some error')))
: sleep(10).then(() => todo),
})
const todos = ['Todo 1', 'bad', 'Todo 3']
const settledPromise = Promise.allSettled(
todos.map((todo) => mutation.mutateAsync(todo)),
)
await vi.advanceTimersByTimeAsync(10)
const results = await settledPromise
expect(results).toEqual([
{ status: 'fulfilled', value: 'Todo 1' },
{ status: 'rejected', reason: Error('Some error') },
{ status: 'fulfilled', value: 'Todo 3' },
])
})
it('should only fire the per-call onSuccess for the last mutate() call', async () => {
const onSuccessPerCall = vi.fn()
const mutation = useMutation({
mutationFn: (todo: string) => sleep(10).then(() => todo),
})
mutation.mutate('Todo 1', { onSuccess: onSuccessPerCall })
mutation.mutate('Todo 2', { onSuccess: onSuccessPerCall })
await vi.advanceTimersByTimeAsync(10)
expect(onSuccessPerCall).toHaveBeenCalledTimes(1)
const [data, variables, onMutateResult, context] =
onSuccessPerCall.mock.calls[0]!
expect(data).toBe('Todo 2')
expect(variables).toBe('Todo 2')
expect(onMutateResult).toBeUndefined()
expect(context.client).toBe(useQueryClient())
})
})
it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => {
const onSuccess = vi.fn()
const mutation = useMutation({
mutationFn: (text: string) => sleep(10).then(() => text.toUpperCase()),
onMutate: (text: string) => ({ startedWith: text }),
onSuccess,
})
mutation.mutate('todo')
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(useQueryClient())
expect(context.meta).toBeUndefined()
expect(context.mutationKey).toBeUndefined()
})
it('should give mutationFn the same QueryClient instance via context', async () => {
const key = queryKey()
const queryClient = useQueryClient()
queryClient.setQueryData(key, 'tag-from-this-client')
const mutation = useMutation({
mutationFn: (_text: string, context: MutationFunctionContext) =>
sleep(10).then(() => context.client.getQueryData(key)),
})
mutation.mutate('todo')
await vi.advanceTimersByTimeAsync(10)
expect(mutation.data.value).toBe('tag-from-this-client')
})
it('should include mutationKey in the context passed to hook-level callbacks', async () => {
const onSuccess = vi.fn()
const mutation = useMutation({
mutationKey: ['todos', 'add'],
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess,
})
mutation.mutate('todo')
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()
const queryClient = useQueryClient()
queryClient.setQueryData(key, 'data')
const mutation = useMutation({
mutationFn: () => sleep(10).then(() => 'mutated'),
onSuccess: (_data, _variables, _onMutateResult, context) => {
context.client.invalidateQueries({ queryKey: key })
},
})
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(false)
mutation.mutate()
await vi.advanceTimersByTimeAsync(10)
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true)
})
})