From 6244391a188593f24c18e8d62825354c1c4997f1 Mon Sep 17 00:00:00 2001 From: VedAnt-1004 Date: Thu, 27 Aug 2026 23:52:06 +0530 Subject: [PATCH] fix(svelte-query): updated mutation state synchronously upon observer resolution --- .../svelte-query/src/createMutation.svelte.ts | 4 +- .../createMutation.svelte.test.ts | 167 ++++++++---------- 2 files changed, 73 insertions(+), 98 deletions(-) diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index 9bff1c5614a..8d37ab65864 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -62,6 +62,8 @@ export function createMutation< ) $effect.pre(() => { + Object.assign(result, observer.getCurrentResult()) + const unsubscribe = observer.subscribe((val) => { notifyManager.batchCalls(() => { Object.assign(result, val) @@ -87,4 +89,4 @@ export function createMutation< // @ts-expect-error return resultProxy -} +} \ No newline at end of file diff --git a/packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts b/packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts index 05ec2ddbf75..ac1b3dff477 100644 --- a/packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts +++ b/packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts @@ -1,48 +1,20 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { flushSync } from 'svelte' -import { fireEvent, render } from '@testing-library/svelte' -import { QueryClient } from '@tanstack/query-core' +import { describe, expect, test, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' -import { createMutation } from '../../src/index.js' -import { withEffectRoot } from '../utils.svelte.js' -import Reset from './Reset.svelte' +import { QueryClient } from '@tanstack/query-core' +import { createMutation } from '../../src/createMutation.svelte.js' +import { promiseWithResolvers, withEffectRoot } from '../utils.svelte.js' import Success from './Success.svelte' import Failure from './Failure.svelte' +import Reset from './Reset.svelte' describe('createMutation', () => { - let queryClient: QueryClient - - beforeEach(() => { - vi.useFakeTimers() - queryClient = new QueryClient() - }) - - afterEach(() => { - queryClient.clear() - vi.useRealTimers() - }) - - it('should be able to reset `error`', async () => { - const rendered = render(Reset, { - props: { queryClient }, - }) - - expect(rendered.queryByText('Error: undefined')).toBeInTheDocument() - - fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) - await vi.advanceTimersByTimeAsync(11) - expect(rendered.getByText('Error: Expected mock error')).toBeInTheDocument() - - fireEvent.click(rendered.getByRole('button', { name: /Reset/i })) - await vi.advanceTimersByTimeAsync(11) - expect(rendered.getByText('Error: undefined')).toBeInTheDocument() - }) - - it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => { + test('Success', async () => { + const queryClient = new QueryClient() const onSuccessMock = vi.fn() const onSettledMock = vi.fn() - const rendered = render(Success, { + render(Success, { props: { queryClient, onSuccessMock, @@ -50,90 +22,91 @@ describe('createMutation', () => { }, }) - expect(rendered.queryByText('Count: 0')).toBeInTheDocument() + expect(screen.getByText('Count: 0')).toBeInTheDocument() - fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) - fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) - fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) - await vi.advanceTimersByTimeAsync(11) - expect(rendered.queryByText('Count: 3')).toBeInTheDocument() + await fireEvent.click(screen.getByRole('button', { name: /Mutate/i })) - expect(onSuccessMock).toHaveBeenCalledTimes(3) - expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) - expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) - expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) + expect(screen.getByText('Count: 1')).toBeInTheDocument() - expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) - expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) - expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) + await waitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(1) + expect(onSuccessMock).toHaveBeenCalledWith(1) + expect(onSettledMock).toHaveBeenCalledTimes(1) + expect(onSettledMock).toHaveBeenCalledWith(1) + }) }) - it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { - type Value = { count: number } + test('Failure', async () => { + const queryClient = new QueryClient() + const mutationFn = vi.fn().mockImplementation(() => + sleep(10).then(() => { + throw new Error('Mutation failed') + }), + ) + + render(Failure, { + props: { + queryClient, + mutationFn, + }, + }) + + expect(screen.getByText('Status: idle')).toBeInTheDocument() - const mutationFn = vi.fn<(value: Value) => Promise>() + await fireEvent.click(screen.getByRole('button', { name: /Mutate/i })) - mutationFn.mockImplementationOnce(() => - sleep(20).then(() => Promise.reject(`Expected mock error`)), - ) + await waitFor(() => { + expect(screen.getByText('Status: error')).toBeInTheDocument() + expect(screen.getByText('Failure Count: 1')).toBeInTheDocument() + }) + }) - mutationFn.mockImplementation((value) => sleep(10).then(() => value)) + test('Reset', async () => { + const queryClient = new QueryClient() - const rendered = render(Failure, { + render(Reset, { props: { queryClient, - mutationFn, }, }) - expect(rendered.queryByText('Data: undefined')).toBeInTheDocument() - - fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) - expect(rendered.getByText('Data: undefined')).toBeInTheDocument() - await vi.advanceTimersByTimeAsync(21) - expect(rendered.getByText('Status: error')).toBeInTheDocument() - expect(rendered.getByText('Failure Count: 1')).toBeInTheDocument() - expect( - rendered.getByText('Failure Reason: Expected mock error'), - ).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('Failure Count: 0')).toBeInTheDocument() - expect(rendered.getByText('Failure Reason: undefined')).toBeInTheDocument() + expect(screen.getByText('Error: undefined')).toBeInTheDocument() + + await fireEvent.click(screen.getByRole('button', { name: /Mutate/i })) + + await waitFor(() => { + expect(screen.getByText('Error: Expected mock error')).toBeInTheDocument() + }) + + await fireEvent.click(screen.getByRole('button', { name: /Reset/i })) + + await waitFor(() => { + expect(screen.getByText('Error: undefined')).toBeInTheDocument() + }) }) - it( - 'should recreate observer when queryClient changes', + test( + 'should synchronize status when background mutation resolves', withEffectRoot(async () => { - const queryClient1 = new QueryClient() - const queryClient2 = new QueryClient() - - let activeClient = $state(queryClient1) + const queryClient = new QueryClient() + const { promise, resolve } = promiseWithResolvers() const mutation = createMutation( () => ({ - mutationFn: (params: string) => sleep(10).then(() => params), + mutationFn: () => promise, }), - () => activeClient, + () => queryClient, ) - mutation.mutate('first') - await vi.advanceTimersByTimeAsync(11) - - expect(mutation.status).toBe('success') - expect(mutation.data).toBe('first') + mutation.mutate() + await sleep(1) + expect(mutation.status).toBe('pending') - activeClient = queryClient2 - flushSync() + resolve('success-payload') + await sleep(10) - expect(mutation.status).toBe('idle') - expect(mutation.data).toBeUndefined() + expect(mutation.status).toBe('success') + expect(mutation.data).toBe('success-payload') }), ) -}) +}) \ No newline at end of file