import { QueryClient } from '@tanstack/query-core'; import { QueryClientStore } from '../query-client.store'; import { getTypeName, getClientType, getClientOptions, shouldShareWindowClient, } from '../client-helpers'; import * as clientHelpers from '../client-helpers'; describe('QueryClientStore', () => { let queryClientStore: QueryClientStore; beforeEach(() => { window.queryClients = new Map(); window.pageQueryClientCount = 0; queryClientStore = new QueryClientStore(); }); afterEach(() => { queryClientStore.dispose(); window.queryClients?.clear(); window.pageQueryClientCount = 0; jest.restoreAllMocks(); }); describe('constructor', () => { it('should create an instance with default QueryClient', () => { expect(queryClientStore).toBeDefined(); expect(queryClientStore.queryClient).toBeInstanceOf(QueryClient); }); it('should create an instance with global options', () => { const appStore = new QueryClientStore('app'); expect(appStore).toBeDefined(); expect(appStore.globalClientOptions).toBe('app'); const pageStore = new QueryClientStore('page'); expect(pageStore).toBeDefined(); expect(pageStore.globalClientOptions).toBe('page'); appStore.dispose(); pageStore.dispose(); }); it('should store global clients in window.queryClients with app version', () => { const appStore = new QueryClientStore('app'); expect(window.queryClients?.has(getTypeName('app'))).toBe(true); const pageStore = new QueryClientStore('page'); expect(window.queryClients?.has(getTypeName('page'))).toBe(true); expect(window.queryClients?.size).toBe(2); pageStore.dispose(); expect(window.queryClients?.has(getTypeName('page'))).toBe(false); appStore.dispose(); expect(window.queryClients?.has(getTypeName('app'))).toBe(true); // App clients persist }); it('should manage page client count and remove page client when count is zero', () => { // Initial state expect(window.pageQueryClientCount).toBe(0); // Create first page client - should increment page count const pageStore1 = new QueryClientStore('page'); expect(window.pageQueryClientCount).toBe(1); // Create second page client - should increment page count const pageStore2 = new QueryClientStore('page'); expect(window.pageQueryClientCount).toBe(2); // Create third page client - should increment page count const pageStore3 = new QueryClientStore('page'); expect(window.pageQueryClientCount).toBe(3); // Dispose first page client - should decrement page count pageStore1.dispose(); expect(window.pageQueryClientCount).toBe(2); // Dispose second page client - should decrement page count pageStore2.dispose(); expect(window.pageQueryClientCount).toBe(1); // Dispose third page client - should decrement page count to 0 and remove page client pageStore3.dispose(); expect(window.pageQueryClientCount).toBe(0); expect(window.queryClients?.has(getTypeName('page'))).toBe(false); expect(window.queryClients?.size).toBe(0); }); it('should support multiple app client versions in window.queryClients', () => { let versionCounter = 0; const mockGetTypeName = jest.fn((type: string) => `${type}-${++versionCounter}.0.0`); jest.spyOn(clientHelpers, 'getTypeName').mockImplementation(mockGetTypeName); // Create multiple app clients with different versions const appStore1 = new QueryClientStore('app'); const appStore2 = new QueryClientStore('app'); // Verify app clients exist with different version keys - page count not affected expect(window.queryClients?.has('app-1.0.0')).toBe(true); expect(window.queryClients?.has('app-2.0.0')).toBe(true); expect(window.pageQueryClientCount).toBe(0); // App stores don't affect page count and app clients should persist after dispose appStore1.dispose(); appStore2.dispose(); expect(window.pageQueryClientCount).toBe(0); expect(window.queryClients?.has('app-1.0.0')).toBe(true); // App clients persist expect(window.queryClients?.has('app-2.0.0')).toBe(true); // App clients persist }); it('should support multiple page client versions in window.queryClients and remove all when count is zero', () => { // Mock getTypeName to return different versions for each call let versionCounter = 0; const mockGetTypeName = jest.fn((_type: string) => `page-${++versionCounter}.0.0`); // Import the module and spy on getTypeName jest.spyOn(clientHelpers, 'getTypeName').mockImplementation(mockGetTypeName); // Initial state expect(window.pageQueryClientCount).toBe(0); // Create multiple page clients with different versions - should increment page count const pageStore1 = new QueryClientStore('page'); expect(window.pageQueryClientCount).toBe(1); const pageStore2 = new QueryClientStore('page'); expect(window.pageQueryClientCount).toBe(2); // Verify page clients exist with different version keys expect(window.queryClients?.has('page-1.0.0')).toBe(true); expect(window.queryClients?.has('page-2.0.0')).toBe(true); // Cleanup and verify page count decreases but clients remain until count reaches 0 pageStore1.dispose(); expect(window.pageQueryClientCount).toBe(1); expect(window.queryClients?.has('page-1.0.0')).toBe(true); // Still in window until count < 1 expect(window.queryClients?.has('page-2.0.0')).toBe(true); pageStore2.dispose(); expect(window.pageQueryClientCount).toBe(0); expect(window.queryClients?.has('page-1.0.0')).toBe(false); // Now removed expect(window.queryClients?.has('page-2.0.0')).toBe(false); // Now removed }); }); describe('dispose', () => { it('should dispose without errors', () => { expect(() => queryClientStore.dispose()).not.toThrow(); }); }); describe('fetchQuery', () => { it('should return cloned data when query succeeds', async () => { const originalData = { test: 'data', nested: { value: 1 } }; const queryConfig = { queryKey: ['client', 'test'], queryFn: () => Promise.resolve(originalData), }; const result = await queryClientStore.fetchQuery(queryConfig); expect(result).toEqual(originalData); expect(result).not.toBe(originalData); // Should be cloned }); it('should use app client when appClient option is true', async () => { // Create an app store to ensure app client exists const appStore = new QueryClientStore('app'); const originalData = { test: 'app-data', nested: { value: 42 } }; const queryConfig = { queryKey: ['client', 'test'], queryFn: () => Promise.resolve(originalData), }; // Spy on the app client's fetchQuery method const appClientSpy = jest.spyOn(appStore.queryClient, 'fetchQuery'); const defaultClientSpy = jest.spyOn(queryClientStore.queryClient, 'fetchQuery'); const result = await queryClientStore.fetchQuery(queryConfig, { appClient: true }); expect(result).toEqual(originalData); expect(result).not.toBe(originalData); // Should be cloned // Should use app client, not the default client expect(appClientSpy).toHaveBeenCalledWith(queryConfig); expect(defaultClientSpy).not.toHaveBeenCalled(); appStore.dispose(); }); }); describe('invalidate', () => { it('should handle empty keys', () => { expect(() => queryClientStore.invalidate([])).not.toThrow(); expect(() => queryClientStore.invalidate(undefined)).not.toThrow(); }); it('should invalidate single query key', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); queryClientStore.invalidate(['client', 'test']); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); }); it('should invalidate multiple query keys', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); queryClientStore.invalidate([ ['client', 'test1'], ['client', 'test2'], ]); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test1'] }); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test2'] }); }); it('should invalidate across all clients including shared clients', () => { const appStore = new QueryClientStore('app'); const appClientSpy = jest.spyOn(appStore.queryClient, 'invalidateQueries'); const defaultClientSpy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); queryClientStore.invalidate(['client', 'test']); expect(defaultClientSpy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); expect(appClientSpy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); appStore.dispose(); }); it('should use invalidationKey when provided in options', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); queryClientStore.invalidate(['client', 'test'], { invalidationKey: ['client', 'custom', 'key'], }); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'custom', 'key'] }); }); it('should respect dedupe option when true', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); const getQueryStateSpy = jest.spyOn(queryClientStore.queryClient, 'getQueryState'); // Mock getQueryState to return already invalidated getQueryStateSpy.mockReturnValue({ isInvalidated: true } as any); queryClientStore.invalidate(['client', 'test'], { dedupe: true }); // Should not invalidate because query is already invalidated expect(spy).not.toHaveBeenCalled(); expect(getQueryStateSpy).toHaveBeenCalledWith(['client', 'test']); }); it('should invalidate when dedupe is true but query is not already invalidated', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'invalidateQueries'); const getQueryStateSpy = jest.spyOn(queryClientStore.queryClient, 'getQueryState'); // Mock getQueryState to return not invalidated getQueryStateSpy.mockReturnValue({ isInvalidated: false } as any); queryClientStore.invalidate(['client', 'test'], { dedupe: true }); // Should invalidate because query is not already invalidated expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); expect(getQueryStateSpy).toHaveBeenCalledWith(['client', 'test']); }); }); describe('cancel', () => { it('should handle empty keys', () => { expect(() => queryClientStore.cancel([])).not.toThrow(); expect(() => queryClientStore.cancel(undefined)).not.toThrow(); }); it('should cancel queries', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'cancelQueries'); queryClientStore.cancel(['client', 'test']); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); }); it('should use app client when appClient option is true', () => { // Create an app store to ensure app client exists const appStore = new QueryClientStore('app'); const appClientSpy = jest.spyOn(appStore.queryClient, 'cancelQueries'); const defaultClientSpy = jest.spyOn(queryClientStore.queryClient, 'cancelQueries'); queryClientStore.cancel(['client', 'test'], { appClient: true }); // Should use app client, not the default client expect(appClientSpy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); expect(defaultClientSpy).not.toHaveBeenCalled(); appStore.dispose(); }); }); describe('remove', () => { it('should handle empty keys', () => { expect(() => queryClientStore.remove([])).not.toThrow(); expect(() => queryClientStore.remove(undefined)).not.toThrow(); }); it('should remove queries', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'removeQueries'); queryClientStore.remove(['client', 'test']); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test'] }); }); it('should remove multiple queries', () => { const spy = jest.spyOn(queryClientStore.queryClient, 'removeQueries'); queryClientStore.remove([ ['client', 'test1'], ['client', 'test2'], ]); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test1'] }); expect(spy).toHaveBeenCalledWith({ queryKey: ['client', 'test2'] }); }); }); }); describe('getClientOptions', () => { it('should return undefined for no options', () => { expect(getClientOptions()).toBeUndefined(); expect(getClientOptions(undefined)).toBeUndefined(); }); it('should return type object for string option', () => { expect(getClientOptions('app')).toEqual({ type: 'app' }); expect(getClientOptions('page')).toEqual({ type: 'page' }); }); it('should return the same object for object option', () => { const options = { type: 'app' as const, dispose: true }; expect(getClientOptions(options)).toBe(options); }); }); describe('getClientType', () => { it('should return empty string for undefined options', () => { expect(getClientType()).toBe(''); expect(getClientType(undefined)).toBe(''); }); it('should return client type for string options', () => { expect(getClientType('app')).toBe('app'); expect(getClientType('page')).toBe('page'); }); it('should return client type for object options', () => { expect(getClientType({ type: 'app' })).toBe('app'); expect(getClientType({ type: 'page', dispose: true })).toBe('page'); }); }); describe('shouldShareWindowClient', () => { it('should be true only when globalClient is set and isolate is unset', () => { expect(shouldShareWindowClient()).toBe(false); expect(shouldShareWindowClient('app')).toBe(true); expect(shouldShareWindowClient('app', {})).toBe(true); expect(shouldShareWindowClient('app', { isolateFromWindowClients: false })).toBe(true); expect(shouldShareWindowClient('app', { isolateFromWindowClients: true })).toBe(false); expect(shouldShareWindowClient(undefined, { isolateFromWindowClients: true })).toBe(false); }); });