From 6f853aa79d497fe1e8f950437f00e5b63ee36b06 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Tue, 8 Sep 2026 10:18:11 +0900 Subject: [PATCH 1/4] test(vue-query/useMutation): add tests for 'MutationFunctionContext' passed to mutationFn and callbacks --- .../src/__tests__/useMutation.test-d.tsx | 51 +++++++++++ .../src/__tests__/useMutation.test.ts | 89 +++++++++++++++++-- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/packages/vue-query/src/__tests__/useMutation.test-d.tsx b/packages/vue-query/src/__tests__/useMutation.test-d.tsx index 80d8996e982..8377343ef84 100644 --- a/packages/vue-query/src/__tests__/useMutation.test-d.tsx +++ b/packages/vue-query/src/__tests__/useMutation.test-d.tsx @@ -2,6 +2,7 @@ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' +import type { MutationFunctionContext, MutationKey } from '@tanstack/query-core' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { @@ -96,4 +97,54 @@ describe('useMutation', () => { expectTypeOf(mutation.mutate).toBeCallableWith() expectTypeOf(mutation.mutateAsync).toBeCallableWith() }) + + it('should type context as the last argument for every hook-level callback', () => { + // `mutationFn`'s `context` parameter is implicitly `any` here, unlike every other adapter and unlike + // this file's other callbacks — so it's excluded from the assertions below. + useMutation({ + mutationFn: () => Promise.resolve('data'), + onMutate: (_variables, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onSuccess: (_data, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onError: (_error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + }) + }) + + it('should type context as the last argument for every per-call mutate option', () => { + const mutation = useMutation({ + mutationFn: () => Promise.resolve('data'), + }) + + mutation.mutate(undefined, { + onSuccess: (_data, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onError: (_error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + }) + }) + + it('should type context.mutationKey as MutationKey', () => { + useMutation({ + mutationKey: ['todos', 'add'] as const, + mutationFn: () => Promise.resolve('data'), + onSuccess: (_data, _variables, _onMutateResult, context) => { + expectTypeOf(context.mutationKey).toEqualTypeOf< + MutationKey | undefined + >() + }, + }) + }) }) diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts index 7b1f889eaa3..92adddabdca 100644 --- a/packages/vue-query/src/__tests__/useMutation.test.ts +++ b/packages/vue-query/src/__tests__/useMutation.test.ts @@ -3,6 +3,7 @@ import { reactive, ref } from 'vue-demi' 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') @@ -560,12 +561,88 @@ describe('useMutation', () => { await vi.advanceTimersByTimeAsync(10) expect(onSuccessPerCall).toHaveBeenCalledTimes(1) - expect(onSuccessPerCall).toHaveBeenCalledWith( - 'Todo 2', - 'Todo 2', - undefined, - expect.anything(), - ) + 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) }) }) From db88c605a17539dac81cf8f785db520c63dbd99d Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Tue, 8 Sep 2026 20:19:37 +0900 Subject: [PATCH 2/4] test(vue-query/useMutation): assert 'context' type for 'mutationFn' as well --- .../src/__tests__/useMutation.test-d.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/vue-query/src/__tests__/useMutation.test-d.tsx b/packages/vue-query/src/__tests__/useMutation.test-d.tsx index 8377343ef84..d0291bb3943 100644 --- a/packages/vue-query/src/__tests__/useMutation.test-d.tsx +++ b/packages/vue-query/src/__tests__/useMutation.test-d.tsx @@ -2,7 +2,11 @@ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' -import type { MutationFunctionContext, MutationKey } from '@tanstack/query-core' +import type { + MutationFunctionContext, + MutationKey, + QueryClient, +} from '@tanstack/query-core' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { @@ -98,11 +102,13 @@ describe('useMutation', () => { expectTypeOf(mutation.mutateAsync).toBeCallableWith() }) - it('should type context as the last argument for every hook-level callback', () => { - // `mutationFn`'s `context` parameter is implicitly `any` here, unlike every other adapter and unlike - // this file's other callbacks — so it's excluded from the assertions below. + it('should type context as the last argument for mutationFn and every hook-level callback', () => { useMutation({ - mutationFn: () => Promise.resolve('data'), + mutationFn: (_vars: string, context) => { + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context.client).toEqualTypeOf() + return Promise.resolve('data') + }, onMutate: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() }, From 3874bdc9af89f1f53d90c9cdfd629dade7256bc3 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Tue, 8 Sep 2026 20:40:49 +0900 Subject: [PATCH 3/4] test(vue-query/useMutation): drop redundant type annotation from 'mutationFn' params in context test --- packages/vue-query/src/__tests__/useMutation.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/src/__tests__/useMutation.test-d.tsx b/packages/vue-query/src/__tests__/useMutation.test-d.tsx index d0291bb3943..bdb58feb075 100644 --- a/packages/vue-query/src/__tests__/useMutation.test-d.tsx +++ b/packages/vue-query/src/__tests__/useMutation.test-d.tsx @@ -104,7 +104,7 @@ describe('useMutation', () => { it('should type context as the last argument for mutationFn and every hook-level callback', () => { useMutation({ - mutationFn: (_vars: string, context) => { + mutationFn: (_vars, context) => { expectTypeOf(context).toEqualTypeOf() expectTypeOf(context.client).toEqualTypeOf() return Promise.resolve('data') From 921e4a1e1a1ac91b2e5cd7ed789372c6c56ef360 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Tue, 8 Sep 2026 21:07:37 +0900 Subject: [PATCH 4/4] test(vue-query/useMutation): rename 'mutationFn' param to '_variables' for consistency --- packages/vue-query/src/__tests__/useMutation.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/src/__tests__/useMutation.test-d.tsx b/packages/vue-query/src/__tests__/useMutation.test-d.tsx index bdb58feb075..a3352f93d5a 100644 --- a/packages/vue-query/src/__tests__/useMutation.test-d.tsx +++ b/packages/vue-query/src/__tests__/useMutation.test-d.tsx @@ -104,7 +104,7 @@ describe('useMutation', () => { it('should type context as the last argument for mutationFn and every hook-level callback', () => { useMutation({ - mutationFn: (_vars, context) => { + mutationFn: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() expectTypeOf(context.client).toEqualTypeOf() return Promise.resolve('data')