From 0bfde3963eb6a895329d21ae5786afab7aeb94b7 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Tue, 8 Sep 2026 09:48:10 +0900 Subject: [PATCH] test(solid-query/useMutation): add tests for 'MutationFunctionContext' passed to mutationFn and callbacks --- .../src/__tests__/useMutation.test-d.tsx | 59 +++++++++- .../src/__tests__/useMutation.test.tsx | 101 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/packages/solid-query/src/__tests__/useMutation.test-d.tsx b/packages/solid-query/src/__tests__/useMutation.test-d.tsx index b5ae427d6e8..6bf8c468e5f 100644 --- a/packages/solid-query/src/__tests__/useMutation.test-d.tsx +++ b/packages/solid-query/src/__tests__/useMutation.test-d.tsx @@ -1,7 +1,12 @@ import { describe, expectTypeOf, it } from 'vitest' import { useMutation } from '../useMutation' import { QueryClient } from '../QueryClient' -import type { DefaultError } from '@tanstack/query-core' +import type { + DefaultError, + MutationFunctionContext, + MutationKey, + QueryClient as QueryCoreClient, +} from '@tanstack/query-core' import type { UseMutationResult } from '../types' describe('useMutation', () => { @@ -144,4 +149,56 @@ describe('useMutation', () => { expectTypeOf(mutation.data).toEqualTypeOf() }) + + it('should type context as the last argument for mutationFn and every hook-level callback', () => { + useMutation(() => ({ + mutationFn: (_vars: string, context) => { + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context.client).toEqualTypeOf() + return 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/solid-query/src/__tests__/useMutation.test.tsx b/packages/solid-query/src/__tests__/useMutation.test.tsx index 2ec3e60088e..495dc1290c2 100644 --- a/packages/solid-query/src/__tests__/useMutation.test.tsx +++ b/packages/solid-query/src/__tests__/useMutation.test.tsx @@ -1741,4 +1741,105 @@ describe('useMutation', () => { ), ).toBeInTheDocument() }) + + it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => { + const onSuccess = vi.fn() + + function Page() { + const mutation = 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 mutation = useMutation(() => ({ + mutationFn: (_text: string, context) => + sleep(10).then(() => context.client.getQueryData(key)), + })) + + return ( +
+
data: {String(mutation.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 mutation = 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 mutation = 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) + }) })