From c3be7acfd6920fe310839a306a9ae6ee79e93b8d Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 4 Jun 2026 10:51:37 +0900 Subject: [PATCH 1/5] test(query-persist-client-core/persist): add test for hydrating a valid persisted cache in 'persistQueryClientRestore' (#10872) --- .../src/__tests__/persist.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/query-persist-client-core/src/__tests__/persist.test.ts b/packages/query-persist-client-core/src/__tests__/persist.test.ts index c831c8b8b02..779d887f522 100644 --- a/packages/query-persist-client-core/src/__tests__/persist.test.ts +++ b/packages/query-persist-client-core/src/__tests__/persist.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { QueriesObserver, QueryClient } from '@tanstack/query-core' +import { QueriesObserver, QueryClient, dehydrate } from '@tanstack/query-core' import { persistQueryClientRestore, persistQueryClientSubscribe, @@ -155,4 +155,27 @@ describe('persistQueryClientRestore', () => { }), ).rejects.toBe(removeError) }) + + it('should hydrate the query client when the persisted cache is valid', async () => { + const sourceClient = new QueryClient() + sourceClient.setQueryData(['key'], 'data') + + const queryClient = new QueryClient() + const persister = createSpyPersister() + + persister.restoreClient = () => + Promise.resolve({ + buster: '', + clientState: dehydrate(sourceClient), + timestamp: Date.now(), + }) + + await persistQueryClientRestore({ + queryClient, + persister, + }) + + expect(persister.removeClient).not.toHaveBeenCalled() + expect(queryClient.getQueryData(['key'])).toBe('data') + }) }) From a6fda44c0de0830c8fed36b76b26e693531128bf Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 4 Jun 2026 11:19:24 +0900 Subject: [PATCH 2/5] test(query-persist-client-core/persist): hoist 'queryClient' and 'persister' setup into 'beforeEach' and clear in 'afterEach' (#10874) --- .../src/__tests__/persist.test.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/query-persist-client-core/src/__tests__/persist.test.ts b/packages/query-persist-client-core/src/__tests__/persist.test.ts index 779d887f522..4a7f5528491 100644 --- a/packages/query-persist-client-core/src/__tests__/persist.test.ts +++ b/packages/query-persist-client-core/src/__tests__/persist.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueriesObserver, QueryClient, dehydrate } from '@tanstack/query-core' import { persistQueryClientRestore, @@ -64,6 +64,18 @@ describe('persistQueryClientSave', () => { }) describe('persistQueryClientRestore', () => { + let queryClient: QueryClient + let persister: ReturnType + + beforeEach(() => { + queryClient = new QueryClient() + persister = createSpyPersister() + }) + + afterEach(() => { + queryClient.clear() + }) + it('should rethrow exceptions in `restoreClient`', async () => { const consoleMock = vi .spyOn(console, 'error') @@ -73,12 +85,8 @@ describe('persistQueryClientRestore', () => { .spyOn(console, 'warn') .mockImplementation(() => undefined) - const queryClient = new QueryClient() - const restoreError = new Error('Error restoring client') - const persister = createSpyPersister() - persister.restoreClient = () => Promise.reject(restoreError) await expect( @@ -105,13 +113,9 @@ describe('persistQueryClientRestore', () => { .spyOn(console, 'warn') .mockImplementation(() => undefined) - const queryClient = new QueryClient() - const restoreError = new Error('Error restoring client') const removeError = new Error('Error removing client') - const persister = createSpyPersister() - persister.restoreClient = () => Promise.reject(restoreError) persister.removeClient = () => Promise.reject(removeError) @@ -131,9 +135,6 @@ describe('persistQueryClientRestore', () => { }) it('should rethrow error in `removeClient`', async () => { - const queryClient = new QueryClient() - - const persister = createSpyPersister() const removeError = new Error('Error removing client') persister.removeClient = () => Promise.reject(removeError) @@ -160,9 +161,6 @@ describe('persistQueryClientRestore', () => { const sourceClient = new QueryClient() sourceClient.setQueryData(['key'], 'data') - const queryClient = new QueryClient() - const persister = createSpyPersister() - persister.restoreClient = () => Promise.resolve({ buster: '', From e6c26c066b43fdfb3a8c394a5dfa7b6d94a0624c Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 4 Jun 2026 12:06:37 +0900 Subject: [PATCH 3/5] test(query-persist-client-core/persist): wrap the suites in a top-level 'persist' describe and hoist the shared 'queryClient' setup (#10875) --- .../src/__tests__/persist.test.ts | 268 +++++++++--------- 1 file changed, 135 insertions(+), 133 deletions(-) diff --git a/packages/query-persist-client-core/src/__tests__/persist.test.ts b/packages/query-persist-client-core/src/__tests__/persist.test.ts index 4a7f5528491..efa9fd9cb10 100644 --- a/packages/query-persist-client-core/src/__tests__/persist.test.ts +++ b/packages/query-persist-client-core/src/__tests__/persist.test.ts @@ -6,174 +6,176 @@ import { } from '../persist' import { createMockPersister, createSpyPersister } from './utils' -describe('persistQueryClientSubscribe', () => { - it('should persist mutations', async () => { - const queryClient = new QueryClient() +describe('persist', () => { + let queryClient: QueryClient - const persister = createMockPersister() + beforeEach(() => { + queryClient = new QueryClient() + }) - const unsubscribe = persistQueryClientSubscribe({ - queryClient, - persister, - dehydrateOptions: { shouldDehydrateMutation: () => true }, - }) + afterEach(() => { + queryClient.clear() + }) - queryClient.getMutationCache().build(queryClient, { - mutationFn: (text: string) => Promise.resolve(text), - }) + describe('persistQueryClientSubscribe', () => { + it('should persist mutations', async () => { + const persister = createMockPersister() - const result = await persister.restoreClient() + const unsubscribe = persistQueryClientSubscribe({ + queryClient, + persister, + dehydrateOptions: { shouldDehydrateMutation: () => true }, + }) - expect(result?.clientState.mutations).toHaveLength(1) + queryClient.getMutationCache().build(queryClient, { + mutationFn: (text: string) => Promise.resolve(text), + }) + + const result = await persister.restoreClient() + + expect(result?.clientState.mutations).toHaveLength(1) - unsubscribe() + unsubscribe() + }) }) -}) -describe('persistQueryClientSave', () => { - it('should not be triggered on observer type events', () => { - const queryClient = new QueryClient() + describe('persistQueryClientSave', () => { + it('should not be triggered on observer type events', () => { + const persister = createSpyPersister() - const persister = createSpyPersister() + const unsubscribe = persistQueryClientSubscribe({ + queryClient, + persister, + }) - const unsubscribe = persistQueryClientSubscribe({ - queryClient, - persister, + const queryKey = ['test'] + const queryFn = vi.fn().mockReturnValue(1) + const observer = new QueriesObserver(queryClient, [{ queryKey, queryFn }]) + const unsubscribeObserver = observer.subscribe(vi.fn()) + observer + .getObservers()[0] + ?.setOptions({ queryKey, refetchOnWindowFocus: false }) + unsubscribeObserver() + + queryClient.setQueryData(queryKey, 2) + + // persistClient should be called 3 times: + // 1. When query is added + // 2. When queryFn is resolved + // 3. When setQueryData is called + // All events fired by manipulating observers are ignored + expect(persister.persistClient).toHaveBeenCalledTimes(3) + + unsubscribe() }) - - const queryKey = ['test'] - const queryFn = vi.fn().mockReturnValue(1) - const observer = new QueriesObserver(queryClient, [{ queryKey, queryFn }]) - const unsubscribeObserver = observer.subscribe(vi.fn()) - observer - .getObservers()[0] - ?.setOptions({ queryKey, refetchOnWindowFocus: false }) - unsubscribeObserver() - - queryClient.setQueryData(queryKey, 2) - - // persistClient should be called 3 times: - // 1. When query is added - // 2. When queryFn is resolved - // 3. When setQueryData is called - // All events fired by manipulating observers are ignored - expect(persister.persistClient).toHaveBeenCalledTimes(3) - - unsubscribe() }) -}) -describe('persistQueryClientRestore', () => { - let queryClient: QueryClient - let persister: ReturnType + describe('persistQueryClientRestore', () => { + let persister: ReturnType - beforeEach(() => { - queryClient = new QueryClient() - persister = createSpyPersister() - }) + beforeEach(() => { + persister = createSpyPersister() + }) - afterEach(() => { - queryClient.clear() - }) + it('should rethrow exceptions in `restoreClient`', async () => { + const consoleMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) - it('should rethrow exceptions in `restoreClient`', async () => { - const consoleMock = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined) + const consoleWarn = vi + .spyOn(console, 'warn') + .mockImplementation(() => undefined) - const consoleWarn = vi - .spyOn(console, 'warn') - .mockImplementation(() => undefined) + const restoreError = new Error('Error restoring client') - const restoreError = new Error('Error restoring client') + persister.restoreClient = () => Promise.reject(restoreError) - persister.restoreClient = () => Promise.reject(restoreError) + await expect( + persistQueryClientRestore({ + queryClient, + persister, + }), + ).rejects.toBe(restoreError) - await expect( - persistQueryClientRestore({ - queryClient, - persister, - }), - ).rejects.toBe(restoreError) + expect(consoleMock).toHaveBeenCalledTimes(1) + expect(consoleWarn).toHaveBeenCalledTimes(1) + expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) - expect(consoleMock).toHaveBeenCalledTimes(1) - expect(consoleWarn).toHaveBeenCalledTimes(1) - expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) + consoleMock.mockRestore() + consoleWarn.mockRestore() + }) - consoleMock.mockRestore() - consoleWarn.mockRestore() - }) + it('should rethrow exceptions in `removeClient` before `restoreClient`', async () => { + const consoleMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) - it('should rethrow exceptions in `removeClient` before `restoreClient`', async () => { - const consoleMock = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined) + const consoleWarn = vi + .spyOn(console, 'warn') + .mockImplementation(() => undefined) - const consoleWarn = vi - .spyOn(console, 'warn') - .mockImplementation(() => undefined) + const restoreError = new Error('Error restoring client') + const removeError = new Error('Error removing client') - const restoreError = new Error('Error restoring client') - const removeError = new Error('Error removing client') + persister.restoreClient = () => Promise.reject(restoreError) + persister.removeClient = () => Promise.reject(removeError) - persister.restoreClient = () => Promise.reject(restoreError) - persister.removeClient = () => Promise.reject(removeError) + await expect( + persistQueryClientRestore({ + queryClient, + persister, + }), + ).rejects.toBe(removeError) - await expect( - persistQueryClientRestore({ - queryClient, - persister, - }), - ).rejects.toBe(removeError) + expect(consoleMock).toHaveBeenCalledTimes(1) + expect(consoleWarn).toHaveBeenCalledTimes(1) + expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) - expect(consoleMock).toHaveBeenCalledTimes(1) - expect(consoleWarn).toHaveBeenCalledTimes(1) - expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) + consoleMock.mockRestore() + consoleWarn.mockRestore() + }) - consoleMock.mockRestore() - consoleWarn.mockRestore() - }) + it('should rethrow error in `removeClient`', async () => { + const removeError = new Error('Error removing client') + + persister.removeClient = () => Promise.reject(removeError) + persister.restoreClient = () => { + return Promise.resolve({ + buster: 'random-buster', + clientState: { + mutations: [], + queries: [], + }, + timestamp: new Date().getTime(), + }) + } + + await expect( + persistQueryClientRestore({ + queryClient, + persister, + }), + ).rejects.toBe(removeError) + }) - it('should rethrow error in `removeClient`', async () => { - const removeError = new Error('Error removing client') - - persister.removeClient = () => Promise.reject(removeError) - persister.restoreClient = () => { - return Promise.resolve({ - buster: 'random-buster', - clientState: { - mutations: [], - queries: [], - }, - timestamp: new Date().getTime(), - }) - } + it('should hydrate the query client when the persisted cache is valid', async () => { + const sourceClient = new QueryClient() + sourceClient.setQueryData(['key'], 'data') - await expect( - persistQueryClientRestore({ + persister.restoreClient = () => + Promise.resolve({ + buster: '', + clientState: dehydrate(sourceClient), + timestamp: Date.now(), + }) + + await persistQueryClientRestore({ queryClient, persister, - }), - ).rejects.toBe(removeError) - }) - - it('should hydrate the query client when the persisted cache is valid', async () => { - const sourceClient = new QueryClient() - sourceClient.setQueryData(['key'], 'data') - - persister.restoreClient = () => - Promise.resolve({ - buster: '', - clientState: dehydrate(sourceClient), - timestamp: Date.now(), }) - await persistQueryClientRestore({ - queryClient, - persister, + expect(persister.removeClient).not.toHaveBeenCalled() + expect(queryClient.getQueryData(['key'])).toBe('data') }) - - expect(persister.removeClient).not.toHaveBeenCalled() - expect(queryClient.getQueryData(['key'])).toBe('data') }) }) From 89bae56573156bb676dd38f17b96706277a6878b Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 4 Jun 2026 13:34:04 +0900 Subject: [PATCH 4/5] test(query-persist-client-core/persist): add tests for removing an expired or busted cache in 'persistQueryClientRestore' (#10876) --- .../src/__tests__/persist.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/query-persist-client-core/src/__tests__/persist.test.ts b/packages/query-persist-client-core/src/__tests__/persist.test.ts index efa9fd9cb10..ded1c53ac03 100644 --- a/packages/query-persist-client-core/src/__tests__/persist.test.ts +++ b/packages/query-persist-client-core/src/__tests__/persist.test.ts @@ -177,5 +177,39 @@ describe('persist', () => { expect(persister.removeClient).not.toHaveBeenCalled() expect(queryClient.getQueryData(['key'])).toBe('data') }) + + it('should remove the client when the persisted cache is expired', async () => { + persister.restoreClient = () => + Promise.resolve({ + buster: '', + clientState: { mutations: [], queries: [] }, + timestamp: Date.now() - 1000, + }) + + await persistQueryClientRestore({ + queryClient, + persister, + maxAge: 100, + }) + + expect(persister.removeClient).toHaveBeenCalledTimes(1) + }) + + it('should remove the client when the buster does not match', async () => { + persister.restoreClient = () => + Promise.resolve({ + buster: 'old-buster', + clientState: { mutations: [], queries: [] }, + timestamp: Date.now(), + }) + + await persistQueryClientRestore({ + queryClient, + persister, + buster: 'new-buster', + }) + + expect(persister.removeClient).toHaveBeenCalledTimes(1) + }) }) }) From 5fcb4d76a42cd668a7c54bf7ea27268412ac4cca Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 4 Jun 2026 13:48:36 +0900 Subject: [PATCH 5/5] test(query-persist-client-core/persist): add test for removing a cache without a timestamp in 'persistQueryClientRestore' (#10877) --- .../src/__tests__/persist.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/query-persist-client-core/src/__tests__/persist.test.ts b/packages/query-persist-client-core/src/__tests__/persist.test.ts index ded1c53ac03..3138abc9727 100644 --- a/packages/query-persist-client-core/src/__tests__/persist.test.ts +++ b/packages/query-persist-client-core/src/__tests__/persist.test.ts @@ -211,5 +211,21 @@ describe('persist', () => { expect(persister.removeClient).toHaveBeenCalledTimes(1) }) + + it('should remove the client when the persisted cache has no timestamp', async () => { + persister.restoreClient = () => + Promise.resolve({ + buster: '', + clientState: { mutations: [], queries: [] }, + timestamp: 0, + }) + + await persistQueryClientRestore({ + queryClient, + persister, + }) + + expect(persister.removeClient).toHaveBeenCalledTimes(1) + }) }) })