diff --git a/packages/svelte-query/tests/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery.svelte.test.ts deleted file mode 100644 index b0cf691608..0000000000 --- a/packages/svelte-query/tests/createQuery.svelte.test.ts +++ /dev/null @@ -1,1920 +0,0 @@ -import { flushSync } from 'svelte' -import { - afterEach, - beforeEach, - describe, - expect, - expectTypeOf, - it, - vi, -} from 'vitest' -import { queryKey, sleep } from '@tanstack/query-test-utils' -import { QueryClient, createQuery, keepPreviousData } from '../src/index.js' -import { promiseWithResolvers, withEffectRoot } from './utils.svelte.js' -import type { CreateQueryResult, QueryCache } from '../src/index.js' - -describe('createQuery', () => { - let queryClient: QueryClient - let queryCache: QueryCache - - beforeEach(() => { - vi.useFakeTimers() - queryClient = new QueryClient() - queryCache = queryClient.getQueryCache() - }) - - afterEach(() => { - queryClient.clear() - vi.useRealTimers() - }) - - it( - 'should return the correct states for a successful query', - withEffectRoot(async () => { - const key = queryKey() - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - }), - () => queryClient, - ) - - if (query.isPending) { - expectTypeOf(query.data).toEqualTypeOf() - expectTypeOf(query.error).toEqualTypeOf() - } else if (query.isLoadingError) { - expectTypeOf(query.data).toEqualTypeOf() - expectTypeOf(query.error).toEqualTypeOf() - } else { - expectTypeOf(query.data).toEqualTypeOf() - expectTypeOf(query.error).toEqualTypeOf() - } - - const promise1 = query.promise - - expect(query).toEqual({ - data: undefined, - dataUpdatedAt: 0, - error: null, - errorUpdatedAt: 0, - failureCount: 0, - failureReason: null, - errorUpdateCount: 0, - isEnabled: true, - isError: false, - isFetched: false, - isFetchedAfterMount: false, - isFetching: true, - isPaused: false, - isPending: true, - isInitialLoading: true, - isLoading: true, - isLoadingError: false, - isPlaceholderData: false, - isRefetchError: false, - isRefetching: false, - isStale: true, - isSuccess: false, - refetch: expect.any(Function), - status: 'pending', - fetchStatus: 'fetching', - promise: expect.any(Promise), - }) - resolve('resolved') - await vi.advanceTimersByTimeAsync(0) - expect(query).toEqual({ - data: 'resolved', - dataUpdatedAt: expect.any(Number), - error: null, - errorUpdatedAt: 0, - failureCount: 0, - failureReason: null, - errorUpdateCount: 0, - isEnabled: true, - isError: false, - isFetched: true, - isFetchedAfterMount: true, - isFetching: false, - isPaused: false, - isPending: false, - isInitialLoading: false, - isLoading: false, - isLoadingError: false, - isPlaceholderData: false, - isRefetchError: false, - isRefetching: false, - isStale: true, - isSuccess: true, - refetch: expect.any(Function), - status: 'success', - fetchStatus: 'idle', - promise: expect.any(Promise), - }) - - expect(promise1).toBe(query.promise) - }), - ) - - it( - 'should return the correct states for an unsuccessful query', - withEffectRoot(async () => { - const key = queryKey() - let count = 0 - const states: Array = [] - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => { - return Promise.reject(new Error('rejected #' + ++count)) - }, - retry: 1, - retryDelay: 1, - }), - () => queryClient, - ) - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(1) - await vi.advanceTimersByTimeAsync(0) - expect(query.isError).toBe(true) - - expect(states[0]).toEqual({ - data: undefined, - dataUpdatedAt: 0, - error: null, - errorUpdatedAt: 0, - failureCount: 0, - failureReason: null, - errorUpdateCount: 0, - isEnabled: true, - isError: false, - isFetched: false, - isFetchedAfterMount: false, - isFetching: true, - isPaused: false, - isPending: true, - isInitialLoading: true, - isLoading: true, - isLoadingError: false, - isPlaceholderData: false, - isRefetchError: false, - isRefetching: false, - isStale: true, - isSuccess: false, - refetch: expect.any(Function), - status: 'pending', - fetchStatus: 'fetching', - promise: expect.any(Promise), - }) - - expect(states[1]).toEqual({ - data: undefined, - dataUpdatedAt: 0, - error: null, - errorUpdatedAt: 0, - failureCount: 1, - failureReason: new Error('rejected #1'), - errorUpdateCount: 0, - isEnabled: true, - isError: false, - isFetched: false, - isFetchedAfterMount: false, - isFetching: true, - isPaused: false, - isPending: true, - isInitialLoading: true, - isLoading: true, - isLoadingError: false, - isPlaceholderData: false, - isRefetchError: false, - isRefetching: false, - isStale: true, - isSuccess: false, - refetch: expect.any(Function), - status: 'pending', - fetchStatus: 'fetching', - promise: expect.any(Promise), - }) - - expect(states[2]).toEqual({ - data: undefined, - dataUpdatedAt: 0, - error: new Error('rejected #2'), - errorUpdatedAt: expect.any(Number), - failureCount: 2, - failureReason: new Error('rejected #2'), - errorUpdateCount: 1, - isEnabled: true, - isError: true, - isFetched: true, - isFetchedAfterMount: true, - isFetching: false, - isPaused: false, - isPending: false, - isInitialLoading: false, - isLoading: false, - isLoadingError: true, - isPlaceholderData: false, - isRefetchError: false, - isRefetching: false, - isStale: true, - isSuccess: false, - refetch: expect.any(Function), - status: 'error', - fetchStatus: 'idle', - promise: expect.any(Promise), - }) - }), - ) - - it('should set isFetchedAfterMount to true after a query has been fetched', async () => { - const key = queryKey() - - await queryClient.prefetchQuery({ - queryKey: key, - queryFn: () => Promise.resolve('prefetched'), - }) - - await withEffectRoot(async () => { - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - }), - () => queryClient, - ) - - expect(query).toEqual( - expect.objectContaining({ - data: 'prefetched', - isFetched: true, - isFetchedAfterMount: false, - }), - ) - resolve('resolved') - await vi.advanceTimersByTimeAsync(0) - expect(query).toEqual( - expect.objectContaining({ - data: 'resolved', - isFetched: true, - isFetchedAfterMount: true, - }), - ) - })() - }) - - it( - 'should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', - withEffectRoot(async () => { - const key = queryKey() - let fetchCount = 0 - - const { promise, resolve } = promiseWithResolvers() - - const { refetch } = createQuery( - () => ({ - queryKey: key, - queryFn: () => { - fetchCount++ - return promise - }, - enabled: false, - initialData: 'initial', - }), - () => queryClient, - ) - - refetch() - refetch({ cancelRefetch: false }) - - resolve('resolved') - await promise - - expect(fetchCount).toBe(1) - }), - ) - - it( - 'should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', - withEffectRoot(async () => { - const key = queryKey() - let fetchCount = 0 - - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: async () => { - fetchCount++ - return promise - }, - enabled: false, - initialData: 'initialData', - }), - () => queryClient, - ) - - // Trigger two refetch close together - query.refetch() - query.refetch() - - resolve('resolved') - await promise - - expect(fetchCount).toBe(2) - }), - ) - - it( - 'should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', - withEffectRoot(async () => { - const key = queryKey() - let fetchCount = 0 - - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: async () => { - fetchCount++ - return promise - }, - enabled: false, - }), - () => queryClient, - ) - - // Trigger two refetch close together - query.refetch() - query.refetch() - - resolve('resolved') - await promise - - expect(fetchCount).toBe(1) - }), - ) - - it( - 'should be able to watch a query without providing a query function', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - queryClient.setQueryDefaults(key, { - queryFn: () => 'data', - }) - - const query = createQuery( - () => ({ queryKey: key }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('data') - - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined }) - expect(states[1]).toMatchObject({ data: 'data' }) - }), - ) - - it('should pick up a query when re-mounting with gcTime 0', async () => { - const key = queryKey() - // this needs to be split into two different effect roots because - // effects won't pick up dependencies created after the first `await` - // -- the two roots effectively emulate two consecutive components being rendered - await withEffectRoot(async () => { - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - gcTime: 0, - notifyOnChangeProps: 'all', - }), - () => queryClient, - ) - - expect(query).toMatchObject({ - isPending: true, - isSuccess: false, - isFetching: true, - }) - - resolve('resolved: 1') - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('resolved: 1') - - expect(query).toMatchObject({ - isPending: false, - isSuccess: true, - isFetching: false, - }) - })() - - await withEffectRoot(async () => { - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - gcTime: 0, - notifyOnChangeProps: 'all', - }), - () => queryClient, - ) - - expect(query).toMatchObject({ - data: 'resolved: 1', - isPending: false, - isSuccess: true, - isFetching: true, - }) - - resolve('resolved: 2') - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('resolved: 2') - - expect(query).toMatchObject({ - data: 'resolved: 2', - isPending: false, - isSuccess: true, - isFetching: false, - }) - })() - }) - - it('should not get into an infinite loop when removing a query with gcTime 0 and rerendering', async () => { - const key = queryKey() - const states: Array> = [] - - // First mount: render the query and let it fetch - await withEffectRoot(async () => { - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('data'), - gcTime: 0, - notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('data') - })() - - // Simulate rerender by removing the query and mounting again - await withEffectRoot(async () => { - queryClient.removeQueries({ queryKey: key }) - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('data'), - gcTime: 0, - notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('data') - - // Give it time to catch any accidental infinite updates - await vi.advanceTimersByTimeAsync(100) - })() - - expect(states.length).toBe(4) - expect(states[0]).toMatchObject({ - isPending: true, - isSuccess: false, - data: undefined, - }) - expect(states[1]).toMatchObject({ - isPending: false, - isSuccess: true, - data: 'data', - }) - expect(states[2]).toMatchObject({ - isPending: true, - isSuccess: false, - data: undefined, - }) - expect(states[3]).toMatchObject({ - isPending: false, - isSuccess: true, - data: 'data', - }) - }) - - it( - 'should fetch when refetchOnMount is false and nothing has been fetched yet', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => 'test', - refetchOnMount: false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('test') - - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined }) - expect(states[1]).toMatchObject({ data: 'test' }) - }), - ) - - it( - 'should not fetch when refetchOnMount is false and data has been fetched already', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - queryClient.setQueryData(key, 'prefetched') - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => 'test', - refetchOnMount: false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('prefetched') - - expect(states.length).toBe(1) - expect(states[0]).toMatchObject({ data: 'prefetched' }) - }), - ) - - it( - 'should be able to select a part of the data with select', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - const query = createQuery<{ name: string }, Error, string>( - () => ({ - queryKey: key, - queryFn: () => ({ name: 'test' }), - select: (data) => data.name, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('test') - - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined }) - expect(states[1]).toMatchObject({ data: 'test' }) - }), - ) - - it( - 'should throw an error when a selector throws', - withEffectRoot(async () => { - const key = queryKey() - const error = new Error('Select Error') - const states: Array> = [] - - const query = createQuery<{ name: string }, Error, string>( - () => ({ - queryKey: key, - queryFn: () => ({ name: 'test' }), - select: () => { - throw error - }, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.status).toBe('error') - - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) - expect(states[1]).toMatchObject({ status: 'error', error }) - }), - ) - - it( - 'should be able to remove a query', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => ++count, - notifyOnChangeProps: 'all', - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(1) - - queryClient.removeQueries({ queryKey: key }) - query.refetch() - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(2) - - expect(states.length).toBe(4) - expect(states[0]).toMatchObject({ - status: 'pending', - data: undefined, - dataUpdatedAt: 0, - }) - expect(states[1]).toMatchObject({ status: 'success', data: 1 }) - expect(states[2]).toMatchObject({ - status: 'pending', - data: undefined, - dataUpdatedAt: 0, - }) - expect(states[3]).toMatchObject({ status: 'success', data: 2 }) - }), - ) - - it( - 'keeps up-to-date with query key changes', - withEffectRoot(async () => { - const key = queryKey() - let search = $state('') - const states: Array> = [] - - const query = createQuery( - () => ({ - queryKey: [...key, search], - queryFn: async () => Promise.resolve(search), - placeholderData: keepPreviousData, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('') - search = 'phone' - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('phone') - - expect(states.length).toBe(4) - expect(states[0]).toMatchObject({ - status: 'pending', - fetchStatus: 'fetching', - data: undefined, - }) - expect(states[1]).toMatchObject({ - status: 'success', - fetchStatus: 'idle', - data: '', - }) - expect(states[2]).toMatchObject({ - status: 'success', - fetchStatus: 'fetching', - data: '', - }) - expect(states[3]).toMatchObject({ - status: 'success', - fetchStatus: 'idle', - data: 'phone', - }) - }), - ) - - it( - 'should create a new query when refetching a removed query', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve(++count), - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(1) - - queryClient.removeQueries({ queryKey: key }) - query.refetch() - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(2) - - expect(states.length).toBe(4) - // Initial - expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) - // Fetched - expect(states[1]).toMatchObject({ data: 1 }) - // Switch - expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) - // Fetched - expect(states[3]).toMatchObject({ data: 2 }) - }), - ) - - it( - 'should share equal data structures between query results', - withEffectRoot(async () => { - const key = queryKey() - - const result1 = [ - { id: '1', done: false }, - { id: '2', done: false }, - ] - - const result2 = [ - { id: '1', done: false }, - { id: '2', done: true }, - ] - - const states: Array> = [] - - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => { - count++ - return Promise.resolve(count === 1 ? result1 : result2) - }, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data?.[1]?.done).toBe(false) - query.refetch() - await vi.advanceTimersByTimeAsync(0) - expect(query.data?.[1]?.done).toBe(true) - - expect(states.length).toBe(4) - - const todos = states[1]?.data - const todo1 = todos?.[0] - const todo2 = todos?.[1] - - const newTodos = states[3]?.data - const newTodo1 = newTodos?.[0] - const newTodo2 = newTodos?.[1] - - expect(todos).toEqual(result1) - expect(newTodos).toEqual(result2) - expect(newTodos).not.toBe(todos) - expect(newTodo1).toBe(todo1) - expect(newTodo2).not.toBe(todo2) - }), - ) - - it( - 'should use query function from hook when the existing query does not have a query function', - withEffectRoot(async () => { - const key = queryKey() - - queryClient.setQueryData(key, 'set') - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('fetched'), - initialData: 'initial', - staleTime: Infinity, - }), - () => queryClient, - ) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('set') - queryClient.refetchQueries({ queryKey: key }) - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('fetched') - }), - ) - - it( - 'should update query stale state and refetch when invalidated with invalidateQueries', - withEffectRoot(async () => { - const key = queryKey() - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve(++count), - staleTime: Infinity, - }), - () => queryClient, - ) - - await vi.advanceTimersByTimeAsync(0) - expect(query).toEqual( - expect.objectContaining({ - data: 1, - isStale: false, - isFetching: false, - }), - ) - queryClient.invalidateQueries({ queryKey: key }) - await vi.advanceTimersByTimeAsync(0) - expect(query).toEqual( - expect.objectContaining({ - data: 2, - isStale: false, - isFetching: false, - }), - ) - }), - ) - - it( - 'should not update disabled query when refetching with refetchQueries', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve(++count), - enabled: false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - - expect(states.length).toBe(1) - expect(states[0]).toMatchObject({ - data: undefined, - isSuccess: false, - isFetching: false, - isStale: false, - }) - }), - ) - - it( - 'should not refetch disabled query when invalidated with invalidateQueries', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve(++count), - enabled: false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - queryClient.invalidateQueries({ queryKey: key }) - - // Wait long enough for the invalidation and potential refetch - await vi.advanceTimersByTimeAsync(100) - - expect(states.length).toBe(1) - expect(states[0]).toMatchObject({ - data: undefined, - isFetching: false, - isSuccess: false, - isStale: false, - }) - }), - ) - - it( - 'should not fetch when switching to a disabled query', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = $state(0) - - const query = createQuery( - () => ({ - queryKey: [...key, count], - queryFn: () => Promise.resolve(count), - enabled: count === 0, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - count = 1 - await vi.advanceTimersByTimeAsync(0) - expect(states.length).toBe(3) - - // Fetch query - expect(states[0]).toMatchObject({ - isFetching: true, - isSuccess: false, - }) - // Fetched query - expect(states[1]).toMatchObject({ - data: 0, - isFetching: false, - isSuccess: true, - }) - // Switch to disabled query - expect(states[2]).toMatchObject({ - isFetching: false, - isSuccess: false, - }) - }), - ) - - it( - 'should keep the previous data when placeholderData is set', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = $state(0) - - const query = createQuery( - () => ({ - queryKey: [...key, count], - queryFn: () => Promise.resolve(count), - placeholderData: keepPreviousData, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the initial fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - - // Update count to trigger a new fetch - count = 1 - - // Wait for all state updates to complete - await vi.advanceTimersByTimeAsync(0) - expect(states.length).toBe(4) - - // Initial - expect(states[0]).toMatchObject({ - data: undefined, - isFetching: true, - isSuccess: false, - isPlaceholderData: false, - }) - // Fetched - expect(states[1]).toMatchObject({ - data: 0, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - // Set state - expect(states[2]).toMatchObject({ - data: 0, - isFetching: true, - isSuccess: true, - isPlaceholderData: true, - }) - // New data - expect(states[3]).toMatchObject({ - data: 1, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - }), - ) - - it( - 'should not show initial data from next query if placeholderData is set', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = $state(0) - - const query = createQuery( - () => ({ - queryKey: [...key, count], - queryFn: () => Promise.resolve(count), - initialData: 99, - placeholderData: keepPreviousData, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the initial fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - - // Update count to trigger a new fetch - count = 1 - - // Wait for the new fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(1) - - // Wait for all state updates to complete - expect(states.length).toBe(4) - - // Initial - expect(states[0]).toMatchObject({ - data: 99, - isFetching: true, - isSuccess: true, - isPlaceholderData: false, - }) - // Fetched - expect(states[1]).toMatchObject({ - data: 0, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - // Set state - expect(states[2]).toMatchObject({ - data: 99, - isFetching: true, - isSuccess: true, - isPlaceholderData: false, - }) - // New data - expect(states[3]).toMatchObject({ - data: 1, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - }), - ) - - it( - 'should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - // Set initial query data - queryClient.setQueryData([...key, 10], 10) - - let count = $state(10) - - const query = createQuery( - () => ({ - queryKey: [...key, count], - queryFn: () => Promise.resolve(count), - enabled: false, - placeholderData: keepPreviousData, - notifyOnChangeProps: 'all', - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // let that effect ^ run to push the initial state - flushSync() - flushSync(() => (count = 11)) - flushSync(() => (count = 12)) - query.refetch() - // Wait for all operations to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(12) - - // Disabled query - expect(states[0]).toMatchObject({ - data: 10, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - // Set state (11) - expect(states[1]).toMatchObject({ - data: 10, - isFetching: false, - isSuccess: true, - isPlaceholderData: true, - }) - // Set state (12) - expect(states[2]).toMatchObject({ - data: 10, - isFetching: false, - isSuccess: true, - isPlaceholderData: true, - }) - // Refetch - expect(states[3]).toMatchObject({ - data: 10, - isFetching: true, - isSuccess: true, - isPlaceholderData: true, - }) - // Refetch done - expect(states[4]).toMatchObject({ - data: 12, - isFetching: false, - isSuccess: true, - isPlaceholderData: false, - }) - }), - ) - - it( - 'should use the correct query function when components use different configurations', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - const { promise, resolve } = promiseWithResolvers() - - // Simulate FirstComponent - const firstQuery = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...firstQuery }) - }) - - // Simulate SecondComponent - createQuery( - () => ({ - queryKey: key, - queryFn: () => 2, - }), - () => queryClient, - ) - - // Resolve the first query - resolve(1) - - // Wait for the first query to complete - await vi.advanceTimersByTimeAsync(0) - expect(firstQuery.data).toBe(1) - - // Refetch the first query - firstQuery.refetch() - - // Wait for all state updates to complete - await vi.advanceTimersByTimeAsync(0) - expect(states.length).toBe(4) - - expect(states[0]).toMatchObject({ - data: undefined, - }) - expect(states[1]).toMatchObject({ - data: 1, - }) - expect(states[2]).toMatchObject({ - data: 1, - }) - // This state should be 1 instead of 2 - expect(states[3]).toMatchObject({ - data: 1, - }) - }), - ) - - it('should be able to set different stale times for a query', async () => { - const key = queryKey() - const states1: Array> = [] - const states2: Array> = [] - - // Prefetch the query - const prefetchPromise = queryClient.prefetchQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => 'prefetch'), - }) - await vi.advanceTimersByTimeAsync(10) - await prefetchPromise - - expect(queryClient.getQueryState(key)?.data).toBe('prefetch') - // Advance time so secondQuery (staleTime: 10) sees prefetched data as stale - await vi.advanceTimersByTimeAsync(10) - - await withEffectRoot(async () => { - const firstQuery = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('one'), - staleTime: 100, - }), - () => queryClient, - ) - - $effect(() => { - states1.push({ ...firstQuery }) - }) - - const secondQuery = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('two'), - staleTime: 10, - }), - () => queryClient, - ) - - $effect(() => { - states2.push({ ...secondQuery }) - }) - - // Wait for both staleTime to expire (100ms for firstQuery) - await vi.advanceTimersByTimeAsync(101) - expect(firstQuery).toMatchObject({ data: 'two', isStale: true }) - expect(secondQuery).toMatchObject({ data: 'two', isStale: true }) - - expect(states1).toMatchObject([ - // First render - { - data: 'prefetch', - isStale: false, - }, - // Second createQuery started fetching - { - data: 'prefetch', - isStale: false, - }, - // Second createQuery data came in - { - data: 'two', - isStale: false, - }, - // Data became stale after 100ms - { - data: 'two', - isStale: true, - }, - ]) - - expect(states2).toMatchObject([ - // First render, data is stale and starts fetching - { - data: 'prefetch', - isStale: true, - }, - // Second createQuery data came in - { - data: 'two', - isStale: false, - }, - // Data became stale after 10ms - { - data: 'two', - isStale: true, - }, - ]) - })() - }) - - it( - 'should re-render when a query becomes stale', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => 'test', - staleTime: 50, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the query to become stale - await vi.advanceTimersByTimeAsync(51) - - expect(states.length).toBe(3) - expect(states[0]).toMatchObject({ isStale: true }) - expect(states[1]).toMatchObject({ isStale: false }) - expect(states[2]).toMatchObject({ isStale: true }) - }), - ) - - it( - 'should not re-render when it should only re-render on data changes and the data did not change', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - notifyOnChangeProps: ['data'], - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - resolve('test') - - // Refetch the query - setTimeout(() => { - query.refetch() - }, 10) - - await vi.advanceTimersByTimeAsync(10) - expect(states.length).toBe(2) - - expect(states[0]).toMatchObject({ - data: undefined, - status: 'pending', - isFetching: true, - }) - expect(states[1]).toMatchObject({ - data: 'test', - status: 'success', - isFetching: false, - }) - }), - ) - - it( - 'should track properties and only re-render when a tracked property changes', - withEffectRoot(async () => { - const key = queryKey() - const states: Array = [] - const { promise, resolve } = promiseWithResolvers() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - }), - () => queryClient, - ) - - $effect(() => { - states.push(query.data) - }) - - // Resolve the promise after a delay - setTimeout(() => { - resolve('test') - }, 10) - - await vi.advanceTimersByTimeAsync(10) - expect(query.data).toBe('test') - - // Refetch after data is available - setTimeout(() => { - if (query.data) { - query.refetch() - } - }, 20) - - // Wait for refetch to complete - await vi.advanceTimersByTimeAsync(20) - - expect(states.length).toBe(2) - expect(states[0]).toBe(undefined) - expect(states[1]).toBe('test') - }), - ) - - it( - 'should always re-render if we are tracking props but not using any', - withEffectRoot(async () => { - const key = queryKey() - let renderCount = 0 - const states: Array> = [] - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('test'), - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Track changes to the query state - $effect(() => { - // @ts-expect-error - const _ = { ...query } - renderCount++ - }) - - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('test') - - expect(renderCount).toBe(2) - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined }) - expect(states[1]).toMatchObject({ data: 'test' }) - }), - ) - - it( - 'should update query options', - withEffectRoot(() => { - const key = queryKey() - - const queryFn = () => sleep(10).then(() => 'data1') - - // Create two queries with the same key but different options - createQuery( - () => ({ queryKey: key, queryFn, retryDelay: 10 }), - () => queryClient, - ) - - createQuery( - () => ({ queryKey: key, queryFn, retryDelay: 20 }), - () => queryClient, - ) - - // The last options should win - expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20) - }), - ) - - it( - 'should start with status pending, fetchStatus idle if enabled is false', - withEffectRoot(async () => { - const key1 = queryKey() - const key2 = queryKey() - const states1: Array> = [] - const states2: Array> = [] - - const query1 = createQuery( - () => ({ - queryKey: key1, - queryFn: () => 'data', - enabled: false, - }), - () => queryClient, - ) - - const query2 = createQuery( - () => ({ - queryKey: key2, - queryFn: () => 'data', - }), - () => queryClient, - ) - - $effect(() => { - states1.push({ ...query1 }) - }) - - $effect(() => { - states2.push({ ...query2 }) - }) - - // Check initial states - expect(query1.status).toBe('pending') - expect(query1.fetchStatus).toBe('idle') - - // Wait for second query to complete - await vi.advanceTimersByTimeAsync(0) - expect(query2.status).toBe('success') - expect(query2.fetchStatus).toBe('idle') - - // Verify the state transitions for the second query - expect(states2[0]?.status).toBe('pending') - expect(states2[0]?.fetchStatus).toBe('fetching') - }), - ) - - it( - 'should be in "pending" state by default', - withEffectRoot(() => { - const key = queryKey() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => new Promise(() => {}), - }), - () => queryClient, - ) - - expect(query.status).toBe('pending') - }), - ) - - it( - 'should not refetch query on focus when `enabled` is set to `false`', - withEffectRoot(async () => { - const key = queryKey() - const queryFn = vi.fn().mockReturnValue('data') - - const query = createQuery( - () => ({ - queryKey: key, - queryFn, - enabled: false, - }), - () => queryClient, - ) - - // Wait a bit to ensure the query has time to settle - await vi.advanceTimersByTimeAsync(10) - - // Simulate window focus - window.dispatchEvent(new Event('visibilitychange')) - - // Wait a bit more to ensure no refetch happens - await vi.advanceTimersByTimeAsync(10) - - // The query function should not have been called - expect(queryFn).not.toHaveBeenCalled() - - // Data should be undefined since the query is disabled - expect(query.data).toBeUndefined() - }), - ) - - it( - 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => count++, - staleTime: 0, - refetchOnWindowFocus: false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the initial fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - - // Simulate window focus - window.dispatchEvent(new Event('visibilitychange')) - - // Wait a bit to ensure no refetch happens - await vi.advanceTimersByTimeAsync(10) - - // Should only have 2 states: initial and after fetch - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) - expect(states[1]).toMatchObject({ data: 0, isFetching: false }) - - // Count should still be 0 since no refetch occurred - expect(count).toBe(1) - }), - ) - - it( - 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => count++, - staleTime: 0, - refetchOnWindowFocus: () => false, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the initial fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - - // Simulate window focus - window.dispatchEvent(new Event('visibilitychange')) - - // Wait a bit to ensure no refetch happens - await vi.advanceTimersByTimeAsync(10) - - // Should only have 2 states: initial and after fetch - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) - expect(states[1]).toMatchObject({ data: 0, isFetching: false }) - - // Count should still be 0 since no refetch occurred - expect(count).toBe(1) - }), - ) - - it( - 'should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', - withEffectRoot(async () => { - const key = queryKey() - const states: Array> = [] - let count = 0 - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => count++, - staleTime: Infinity, - refetchOnWindowFocus: true, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the initial fetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe(0) - - // Simulate window focus - window.dispatchEvent(new Event('visibilitychange')) - - // Wait a bit to ensure no refetch happens - await vi.advanceTimersByTimeAsync(10) - - // Should only have 2 states: initial and after fetch - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) - expect(states[1]).toMatchObject({ data: 0, isFetching: false }) - - // Count should still be 0 since no refetch occurred - expect(count).toBe(1) - }), - ) - - it('should refetch fresh query when refetchOnMount is set to always', async () => { - const key = queryKey() - const states: Array> = [] - - // Prefetch the query - await queryClient.prefetchQuery({ - queryKey: key, - queryFn: () => 'prefetched', - }) - - await withEffectRoot(async () => { - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => 'data', - refetchOnMount: 'always', - staleTime: Infinity, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the refetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('data') - - // Should have 2 states: initial (with prefetched data) and after refetch - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ - data: 'prefetched', - isStale: false, - isFetching: true, - }) - expect(states[1]).toMatchObject({ - data: 'data', - isStale: false, - isFetching: false, - }) - })() - }) - - it('should refetch stale query when refetchOnMount is set to true', async () => { - const key = queryKey() - const states: Array> = [] - - // Prefetch the query - await queryClient.prefetchQuery({ - queryKey: key, - queryFn: () => 'prefetched', - }) - - await withEffectRoot(async () => { - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => 'data', - refetchOnMount: true, - staleTime: 0, - }), - () => queryClient, - ) - - $effect(() => { - states.push({ ...query }) - }) - - // Wait for the refetch to complete - await vi.advanceTimersByTimeAsync(0) - expect(query.data).toBe('data') - - // Should have 2 states: initial (with prefetched data) and after refetch - expect(states.length).toBe(2) - expect(states[0]).toMatchObject({ - data: 'prefetched', - isStale: true, - isFetching: true, - }) - expect(states[1]).toMatchObject({ - data: 'data', - isStale: true, - isFetching: false, - }) - })() - }) - - it( - 'should set status to error if queryFn throws', - withEffectRoot(async () => { - const key = queryKey() - const consoleMock = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined) - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.reject(new Error('Error test')), - retry: false, - }), - () => queryClient, - ) - - await vi.advanceTimersByTimeAsync(0) - expect(query.status).toBe('error') - expect(query.error?.message).toBe('Error test') - - consoleMock.mockRestore() - }), - ) - - it( - 'should set status to error instead of throwing when error should not be thrown', - withEffectRoot(async () => { - const key = queryKey() - - const query = createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.reject(new Error('Local Error')), - retry: false, - throwOnError: (err) => err.message !== 'Local Error', - }), - () => queryClient, - ) - - await vi.advanceTimersByTimeAsync(0) - expect(query.status).toBe('error') - expect(query.error?.message).toBe('Local Error') - }), - ) - - it( - 'should support changing provided query client', - withEffectRoot(() => { - const queryClient1 = new QueryClient() - const queryClient2 = new QueryClient() - - let currentClient = $state(queryClient1) - - const key = queryKey() - - createQuery( - () => ({ - queryKey: key, - queryFn: () => Promise.resolve('prefetched'), - }), - () => currentClient, - ) - - expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() - - currentClient = queryClient2 - flushSync() - - expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined() - }), - ) -}) diff --git a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts index 540cf29404..b20c5d76fa 100644 --- a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts @@ -1,17 +1,1925 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/svelte' -import { sleep } from '@tanstack/query-test-utils' +import { flushSync } from 'svelte' +import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, + vi, +} from 'vitest' +import { queryKey, sleep } from '@tanstack/query-test-utils' +import { QueryClient, createQuery, keepPreviousData } from '../../src/index.js' +import { promiseWithResolvers, withEffectRoot } from '../utils.svelte.js' import IsRestoringExample from './IsRestoringExample.svelte' +import type { CreateQueryResult, QueryCache } from '../../src/index.js' describe('createQuery', () => { + let queryClient: QueryClient + let queryCache: QueryCache + beforeEach(() => { vi.useFakeTimers() + queryClient = new QueryClient() + queryCache = queryClient.getQueryCache() }) afterEach(() => { + queryClient.clear() vi.useRealTimers() }) + it( + 'should return the correct states for a successful query', + withEffectRoot(async () => { + const key = queryKey() + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + }), + () => queryClient, + ) + + if (query.isPending) { + expectTypeOf(query.data).toEqualTypeOf() + expectTypeOf(query.error).toEqualTypeOf() + } else if (query.isLoadingError) { + expectTypeOf(query.data).toEqualTypeOf() + expectTypeOf(query.error).toEqualTypeOf() + } else { + expectTypeOf(query.data).toEqualTypeOf() + expectTypeOf(query.error).toEqualTypeOf() + } + + const promise1 = query.promise + + expect(query).toEqual({ + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isEnabled: true, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: true, + isPaused: false, + isPending: true, + isInitialLoading: true, + isLoading: true, + isLoadingError: false, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isSuccess: false, + refetch: expect.any(Function), + status: 'pending', + fetchStatus: 'fetching', + promise: expect.any(Promise), + }) + resolve('resolved') + await vi.advanceTimersByTimeAsync(0) + expect(query).toEqual({ + data: 'resolved', + dataUpdatedAt: expect.any(Number), + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isEnabled: true, + isError: false, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isPaused: false, + isPending: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: false, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isSuccess: true, + refetch: expect.any(Function), + status: 'success', + fetchStatus: 'idle', + promise: expect.any(Promise), + }) + + expect(promise1).toBe(query.promise) + }), + ) + + it( + 'should return the correct states for an unsuccessful query', + withEffectRoot(async () => { + const key = queryKey() + let count = 0 + const states: Array = [] + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => { + return Promise.reject(new Error('rejected #' + ++count)) + }, + retry: 1, + retryDelay: 1, + }), + () => queryClient, + ) + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(1) + await vi.advanceTimersByTimeAsync(0) + expect(query.isError).toBe(true) + + expect(states[0]).toEqual({ + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isEnabled: true, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: true, + isPaused: false, + isPending: true, + isInitialLoading: true, + isLoading: true, + isLoadingError: false, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isSuccess: false, + refetch: expect.any(Function), + status: 'pending', + fetchStatus: 'fetching', + promise: expect.any(Promise), + }) + + expect(states[1]).toEqual({ + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 1, + failureReason: new Error('rejected #1'), + errorUpdateCount: 0, + isEnabled: true, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: true, + isPaused: false, + isPending: true, + isInitialLoading: true, + isLoading: true, + isLoadingError: false, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isSuccess: false, + refetch: expect.any(Function), + status: 'pending', + fetchStatus: 'fetching', + promise: expect.any(Promise), + }) + + expect(states[2]).toEqual({ + data: undefined, + dataUpdatedAt: 0, + error: new Error('rejected #2'), + errorUpdatedAt: expect.any(Number), + failureCount: 2, + failureReason: new Error('rejected #2'), + errorUpdateCount: 1, + isEnabled: true, + isError: true, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isPaused: false, + isPending: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: true, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isSuccess: false, + refetch: expect.any(Function), + status: 'error', + fetchStatus: 'idle', + promise: expect.any(Promise), + }) + }), + ) + + it('should set isFetchedAfterMount to true after a query has been fetched', async () => { + const key = queryKey() + + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('prefetched'), + }) + + await withEffectRoot(async () => { + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + }), + () => queryClient, + ) + + expect(query).toEqual( + expect.objectContaining({ + data: 'prefetched', + isFetched: true, + isFetchedAfterMount: false, + }), + ) + resolve('resolved') + await vi.advanceTimersByTimeAsync(0) + expect(query).toEqual( + expect.objectContaining({ + data: 'resolved', + isFetched: true, + isFetchedAfterMount: true, + }), + ) + })() + }) + + it( + 'should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', + withEffectRoot(async () => { + const key = queryKey() + let fetchCount = 0 + + const { promise, resolve } = promiseWithResolvers() + + const { refetch } = createQuery( + () => ({ + queryKey: key, + queryFn: () => { + fetchCount++ + return promise + }, + enabled: false, + initialData: 'initial', + }), + () => queryClient, + ) + + refetch() + refetch({ cancelRefetch: false }) + + resolve('resolved') + await promise + + expect(fetchCount).toBe(1) + }), + ) + + it( + 'should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', + withEffectRoot(async () => { + const key = queryKey() + let fetchCount = 0 + + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: async () => { + fetchCount++ + return promise + }, + enabled: false, + initialData: 'initialData', + }), + () => queryClient, + ) + + // Trigger two refetch close together + query.refetch() + query.refetch() + + resolve('resolved') + await promise + + expect(fetchCount).toBe(2) + }), + ) + + it( + 'should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', + withEffectRoot(async () => { + const key = queryKey() + let fetchCount = 0 + + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: async () => { + fetchCount++ + return promise + }, + enabled: false, + }), + () => queryClient, + ) + + // Trigger two refetch close together + query.refetch() + query.refetch() + + resolve('resolved') + await promise + + expect(fetchCount).toBe(1) + }), + ) + + it( + 'should be able to watch a query without providing a query function', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + queryClient.setQueryDefaults(key, { + queryFn: () => 'data', + }) + + const query = createQuery( + () => ({ queryKey: key }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('data') + + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined }) + expect(states[1]).toMatchObject({ data: 'data' }) + }), + ) + + it('should pick up a query when re-mounting with gcTime 0', async () => { + const key = queryKey() + // this needs to be split into two different effect roots because + // effects won't pick up dependencies created after the first `await` + // -- the two roots effectively emulate two consecutive components being rendered + await withEffectRoot(async () => { + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + gcTime: 0, + notifyOnChangeProps: 'all', + }), + () => queryClient, + ) + + expect(query).toMatchObject({ + isPending: true, + isSuccess: false, + isFetching: true, + }) + + resolve('resolved: 1') + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('resolved: 1') + + expect(query).toMatchObject({ + isPending: false, + isSuccess: true, + isFetching: false, + }) + })() + + await withEffectRoot(async () => { + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + gcTime: 0, + notifyOnChangeProps: 'all', + }), + () => queryClient, + ) + + expect(query).toMatchObject({ + data: 'resolved: 1', + isPending: false, + isSuccess: true, + isFetching: true, + }) + + resolve('resolved: 2') + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('resolved: 2') + + expect(query).toMatchObject({ + data: 'resolved: 2', + isPending: false, + isSuccess: true, + isFetching: false, + }) + })() + }) + + it('should not get into an infinite loop when removing a query with gcTime 0 and rerendering', async () => { + const key = queryKey() + const states: Array> = [] + + // First mount: render the query and let it fetch + await withEffectRoot(async () => { + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('data'), + gcTime: 0, + notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('data') + })() + + // Simulate rerender by removing the query and mounting again + await withEffectRoot(async () => { + queryClient.removeQueries({ queryKey: key }) + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('data'), + gcTime: 0, + notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('data') + + // Give it time to catch any accidental infinite updates + await vi.advanceTimersByTimeAsync(100) + })() + + expect(states.length).toBe(4) + expect(states[0]).toMatchObject({ + isPending: true, + isSuccess: false, + data: undefined, + }) + expect(states[1]).toMatchObject({ + isPending: false, + isSuccess: true, + data: 'data', + }) + expect(states[2]).toMatchObject({ + isPending: true, + isSuccess: false, + data: undefined, + }) + expect(states[3]).toMatchObject({ + isPending: false, + isSuccess: true, + data: 'data', + }) + }) + + it( + 'should fetch when refetchOnMount is false and nothing has been fetched yet', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => 'test', + refetchOnMount: false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('test') + + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined }) + expect(states[1]).toMatchObject({ data: 'test' }) + }), + ) + + it( + 'should not fetch when refetchOnMount is false and data has been fetched already', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + queryClient.setQueryData(key, 'prefetched') + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => 'test', + refetchOnMount: false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('prefetched') + + expect(states.length).toBe(1) + expect(states[0]).toMatchObject({ data: 'prefetched' }) + }), + ) + + it( + 'should be able to select a part of the data with select', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + const query = createQuery<{ name: string }, Error, string>( + () => ({ + queryKey: key, + queryFn: () => ({ name: 'test' }), + select: (data) => data.name, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('test') + + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined }) + expect(states[1]).toMatchObject({ data: 'test' }) + }), + ) + + it( + 'should throw an error when a selector throws', + withEffectRoot(async () => { + const key = queryKey() + const error = new Error('Select Error') + const states: Array> = [] + + const query = createQuery<{ name: string }, Error, string>( + () => ({ + queryKey: key, + queryFn: () => ({ name: 'test' }), + select: () => { + throw error + }, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.status).toBe('error') + + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) + expect(states[1]).toMatchObject({ status: 'error', error }) + }), + ) + + it( + 'should be able to remove a query', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => ++count, + notifyOnChangeProps: 'all', + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(1) + + queryClient.removeQueries({ queryKey: key }) + query.refetch() + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(2) + + expect(states.length).toBe(4) + expect(states[0]).toMatchObject({ + status: 'pending', + data: undefined, + dataUpdatedAt: 0, + }) + expect(states[1]).toMatchObject({ status: 'success', data: 1 }) + expect(states[2]).toMatchObject({ + status: 'pending', + data: undefined, + dataUpdatedAt: 0, + }) + expect(states[3]).toMatchObject({ status: 'success', data: 2 }) + }), + ) + + it( + 'keeps up-to-date with query key changes', + withEffectRoot(async () => { + const key = queryKey() + let search = $state('') + const states: Array> = [] + + const query = createQuery( + () => ({ + queryKey: [...key, search], + queryFn: async () => Promise.resolve(search), + placeholderData: keepPreviousData, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('') + search = 'phone' + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('phone') + + expect(states.length).toBe(4) + expect(states[0]).toMatchObject({ + status: 'pending', + fetchStatus: 'fetching', + data: undefined, + }) + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: '', + }) + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: '', + }) + expect(states[3]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'phone', + }) + }), + ) + + it( + 'should create a new query when refetching a removed query', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve(++count), + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(1) + + queryClient.removeQueries({ queryKey: key }) + query.refetch() + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(2) + + expect(states.length).toBe(4) + // Initial + expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) + // Fetched + expect(states[1]).toMatchObject({ data: 1 }) + // Switch + expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) + // Fetched + expect(states[3]).toMatchObject({ data: 2 }) + }), + ) + + it( + 'should share equal data structures between query results', + withEffectRoot(async () => { + const key = queryKey() + + const result1 = [ + { id: '1', done: false }, + { id: '2', done: false }, + ] + + const result2 = [ + { id: '1', done: false }, + { id: '2', done: true }, + ] + + const states: Array> = [] + + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => { + count++ + return Promise.resolve(count === 1 ? result1 : result2) + }, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data?.[1]?.done).toBe(false) + query.refetch() + await vi.advanceTimersByTimeAsync(0) + expect(query.data?.[1]?.done).toBe(true) + + expect(states.length).toBe(4) + + const todos = states[1]?.data + const todo1 = todos?.[0] + const todo2 = todos?.[1] + + const newTodos = states[3]?.data + const newTodo1 = newTodos?.[0] + const newTodo2 = newTodos?.[1] + + expect(todos).toEqual(result1) + expect(newTodos).toEqual(result2) + expect(newTodos).not.toBe(todos) + expect(newTodo1).toBe(todo1) + expect(newTodo2).not.toBe(todo2) + }), + ) + + it( + 'should use query function from hook when the existing query does not have a query function', + withEffectRoot(async () => { + const key = queryKey() + + queryClient.setQueryData(key, 'set') + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('fetched'), + initialData: 'initial', + staleTime: Infinity, + }), + () => queryClient, + ) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('set') + queryClient.refetchQueries({ queryKey: key }) + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('fetched') + }), + ) + + it( + 'should update query stale state and refetch when invalidated with invalidateQueries', + withEffectRoot(async () => { + const key = queryKey() + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve(++count), + staleTime: Infinity, + }), + () => queryClient, + ) + + await vi.advanceTimersByTimeAsync(0) + expect(query).toEqual( + expect.objectContaining({ + data: 1, + isStale: false, + isFetching: false, + }), + ) + queryClient.invalidateQueries({ queryKey: key }) + await vi.advanceTimersByTimeAsync(0) + expect(query).toEqual( + expect.objectContaining({ + data: 2, + isStale: false, + isFetching: false, + }), + ) + }), + ) + + it( + 'should not update disabled query when refetching with refetchQueries', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve(++count), + enabled: false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + + expect(states.length).toBe(1) + expect(states[0]).toMatchObject({ + data: undefined, + isSuccess: false, + isFetching: false, + isStale: false, + }) + }), + ) + + it( + 'should not refetch disabled query when invalidated with invalidateQueries', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve(++count), + enabled: false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + queryClient.invalidateQueries({ queryKey: key }) + + // Wait long enough for the invalidation and potential refetch + await vi.advanceTimersByTimeAsync(100) + + expect(states.length).toBe(1) + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: false, + isSuccess: false, + isStale: false, + }) + }), + ) + + it( + 'should not fetch when switching to a disabled query', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = $state(0) + + const query = createQuery( + () => ({ + queryKey: [...key, count], + queryFn: () => Promise.resolve(count), + enabled: count === 0, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + count = 1 + await vi.advanceTimersByTimeAsync(0) + expect(states.length).toBe(3) + + // Fetch query + expect(states[0]).toMatchObject({ + isFetching: true, + isSuccess: false, + }) + // Fetched query + expect(states[1]).toMatchObject({ + data: 0, + isFetching: false, + isSuccess: true, + }) + // Switch to disabled query + expect(states[2]).toMatchObject({ + isFetching: false, + isSuccess: false, + }) + }), + ) + + it( + 'should keep the previous data when placeholderData is set', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = $state(0) + + const query = createQuery( + () => ({ + queryKey: [...key, count], + queryFn: () => Promise.resolve(count), + placeholderData: keepPreviousData, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the initial fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + + // Update count to trigger a new fetch + count = 1 + + // Wait for all state updates to complete + await vi.advanceTimersByTimeAsync(0) + expect(states.length).toBe(4) + + // Initial + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isSuccess: false, + isPlaceholderData: false, + }) + // Fetched + expect(states[1]).toMatchObject({ + data: 0, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + // Set state + expect(states[2]).toMatchObject({ + data: 0, + isFetching: true, + isSuccess: true, + isPlaceholderData: true, + }) + // New data + expect(states[3]).toMatchObject({ + data: 1, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + }), + ) + + it( + 'should not show initial data from next query if placeholderData is set', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = $state(0) + + const query = createQuery( + () => ({ + queryKey: [...key, count], + queryFn: () => Promise.resolve(count), + initialData: 99, + placeholderData: keepPreviousData, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the initial fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + + // Update count to trigger a new fetch + count = 1 + + // Wait for the new fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(1) + + // Wait for all state updates to complete + expect(states.length).toBe(4) + + // Initial + expect(states[0]).toMatchObject({ + data: 99, + isFetching: true, + isSuccess: true, + isPlaceholderData: false, + }) + // Fetched + expect(states[1]).toMatchObject({ + data: 0, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + // Set state + expect(states[2]).toMatchObject({ + data: 99, + isFetching: true, + isSuccess: true, + isPlaceholderData: false, + }) + // New data + expect(states[3]).toMatchObject({ + data: 1, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + }), + ) + + it( + 'should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + // Set initial query data + queryClient.setQueryData([...key, 10], 10) + + let count = $state(10) + + const query = createQuery( + () => ({ + queryKey: [...key, count], + queryFn: () => Promise.resolve(count), + enabled: false, + placeholderData: keepPreviousData, + notifyOnChangeProps: 'all', + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // let that effect ^ run to push the initial state + flushSync() + flushSync(() => (count = 11)) + flushSync(() => (count = 12)) + query.refetch() + // Wait for all operations to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(12) + + // Disabled query + expect(states[0]).toMatchObject({ + data: 10, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + // Set state (11) + expect(states[1]).toMatchObject({ + data: 10, + isFetching: false, + isSuccess: true, + isPlaceholderData: true, + }) + // Set state (12) + expect(states[2]).toMatchObject({ + data: 10, + isFetching: false, + isSuccess: true, + isPlaceholderData: true, + }) + // Refetch + expect(states[3]).toMatchObject({ + data: 10, + isFetching: true, + isSuccess: true, + isPlaceholderData: true, + }) + // Refetch done + expect(states[4]).toMatchObject({ + data: 12, + isFetching: false, + isSuccess: true, + isPlaceholderData: false, + }) + }), + ) + + it( + 'should use the correct query function when components use different configurations', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + const { promise, resolve } = promiseWithResolvers() + + // Simulate FirstComponent + const firstQuery = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...firstQuery }) + }) + + // Simulate SecondComponent + createQuery( + () => ({ + queryKey: key, + queryFn: () => 2, + }), + () => queryClient, + ) + + // Resolve the first query + resolve(1) + + // Wait for the first query to complete + await vi.advanceTimersByTimeAsync(0) + expect(firstQuery.data).toBe(1) + + // Refetch the first query + firstQuery.refetch() + + // Wait for all state updates to complete + await vi.advanceTimersByTimeAsync(0) + expect(states.length).toBe(4) + + expect(states[0]).toMatchObject({ + data: undefined, + }) + expect(states[1]).toMatchObject({ + data: 1, + }) + expect(states[2]).toMatchObject({ + data: 1, + }) + // This state should be 1 instead of 2 + expect(states[3]).toMatchObject({ + data: 1, + }) + }), + ) + + it('should be able to set different stale times for a query', async () => { + const key = queryKey() + const states1: Array> = [] + const states2: Array> = [] + + // Prefetch the query + const prefetchPromise = queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'prefetch'), + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise + + expect(queryClient.getQueryState(key)?.data).toBe('prefetch') + // Advance time so secondQuery (staleTime: 10) sees prefetched data as stale + await vi.advanceTimersByTimeAsync(10) + + await withEffectRoot(async () => { + const firstQuery = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('one'), + staleTime: 100, + }), + () => queryClient, + ) + + $effect(() => { + states1.push({ ...firstQuery }) + }) + + const secondQuery = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('two'), + staleTime: 10, + }), + () => queryClient, + ) + + $effect(() => { + states2.push({ ...secondQuery }) + }) + + // Wait for both staleTime to expire (100ms for firstQuery) + await vi.advanceTimersByTimeAsync(101) + expect(firstQuery).toMatchObject({ data: 'two', isStale: true }) + expect(secondQuery).toMatchObject({ data: 'two', isStale: true }) + + expect(states1).toMatchObject([ + // First render + { + data: 'prefetch', + isStale: false, + }, + // Second createQuery started fetching + { + data: 'prefetch', + isStale: false, + }, + // Second createQuery data came in + { + data: 'two', + isStale: false, + }, + // Data became stale after 100ms + { + data: 'two', + isStale: true, + }, + ]) + + expect(states2).toMatchObject([ + // First render, data is stale and starts fetching + { + data: 'prefetch', + isStale: true, + }, + // Second createQuery data came in + { + data: 'two', + isStale: false, + }, + // Data became stale after 10ms + { + data: 'two', + isStale: true, + }, + ]) + })() + }) + + it( + 'should re-render when a query becomes stale', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => 'test', + staleTime: 50, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the query to become stale + await vi.advanceTimersByTimeAsync(51) + + expect(states.length).toBe(3) + expect(states[0]).toMatchObject({ isStale: true }) + expect(states[1]).toMatchObject({ isStale: false }) + expect(states[2]).toMatchObject({ isStale: true }) + }), + ) + + it( + 'should not re-render when it should only re-render on data changes and the data did not change', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + notifyOnChangeProps: ['data'], + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + resolve('test') + + // Refetch the query + setTimeout(() => { + query.refetch() + }, 10) + + await vi.advanceTimersByTimeAsync(10) + expect(states.length).toBe(2) + + expect(states[0]).toMatchObject({ + data: undefined, + status: 'pending', + isFetching: true, + }) + expect(states[1]).toMatchObject({ + data: 'test', + status: 'success', + isFetching: false, + }) + }), + ) + + it( + 'should track properties and only re-render when a tracked property changes', + withEffectRoot(async () => { + const key = queryKey() + const states: Array = [] + const { promise, resolve } = promiseWithResolvers() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + }), + () => queryClient, + ) + + $effect(() => { + states.push(query.data) + }) + + // Resolve the promise after a delay + setTimeout(() => { + resolve('test') + }, 10) + + await vi.advanceTimersByTimeAsync(10) + expect(query.data).toBe('test') + + // Refetch after data is available + setTimeout(() => { + if (query.data) { + query.refetch() + } + }, 20) + + // Wait for refetch to complete + await vi.advanceTimersByTimeAsync(20) + + expect(states.length).toBe(2) + expect(states[0]).toBe(undefined) + expect(states[1]).toBe('test') + }), + ) + + it( + 'should always re-render if we are tracking props but not using any', + withEffectRoot(async () => { + const key = queryKey() + let renderCount = 0 + const states: Array> = [] + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('test'), + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Track changes to the query state + $effect(() => { + // @ts-expect-error + const _ = { ...query } + renderCount++ + }) + + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('test') + + expect(renderCount).toBe(2) + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined }) + expect(states[1]).toMatchObject({ data: 'test' }) + }), + ) + + it( + 'should update query options', + withEffectRoot(() => { + const key = queryKey() + + const queryFn = () => sleep(10).then(() => 'data1') + + // Create two queries with the same key but different options + createQuery( + () => ({ queryKey: key, queryFn, retryDelay: 10 }), + () => queryClient, + ) + + createQuery( + () => ({ queryKey: key, queryFn, retryDelay: 20 }), + () => queryClient, + ) + + // The last options should win + expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20) + }), + ) + + it( + 'should start with status pending, fetchStatus idle if enabled is false', + withEffectRoot(async () => { + const key1 = queryKey() + const key2 = queryKey() + const states1: Array> = [] + const states2: Array> = [] + + const query1 = createQuery( + () => ({ + queryKey: key1, + queryFn: () => 'data', + enabled: false, + }), + () => queryClient, + ) + + const query2 = createQuery( + () => ({ + queryKey: key2, + queryFn: () => 'data', + }), + () => queryClient, + ) + + $effect(() => { + states1.push({ ...query1 }) + }) + + $effect(() => { + states2.push({ ...query2 }) + }) + + // Check initial states + expect(query1.status).toBe('pending') + expect(query1.fetchStatus).toBe('idle') + + // Wait for second query to complete + await vi.advanceTimersByTimeAsync(0) + expect(query2.status).toBe('success') + expect(query2.fetchStatus).toBe('idle') + + // Verify the state transitions for the second query + expect(states2[0]?.status).toBe('pending') + expect(states2[0]?.fetchStatus).toBe('fetching') + }), + ) + + it( + 'should be in "pending" state by default', + withEffectRoot(() => { + const key = queryKey() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => new Promise(() => {}), + }), + () => queryClient, + ) + + expect(query.status).toBe('pending') + }), + ) + + it( + 'should not refetch query on focus when `enabled` is set to `false`', + withEffectRoot(async () => { + const key = queryKey() + const queryFn = vi.fn().mockReturnValue('data') + + const query = createQuery( + () => ({ + queryKey: key, + queryFn, + enabled: false, + }), + () => queryClient, + ) + + // Wait a bit to ensure the query has time to settle + await vi.advanceTimersByTimeAsync(10) + + // Simulate window focus + window.dispatchEvent(new Event('visibilitychange')) + + // Wait a bit more to ensure no refetch happens + await vi.advanceTimersByTimeAsync(10) + + // The query function should not have been called + expect(queryFn).not.toHaveBeenCalled() + + // Data should be undefined since the query is disabled + expect(query.data).toBeUndefined() + }), + ) + + it( + 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => count++, + staleTime: 0, + refetchOnWindowFocus: false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the initial fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + + // Simulate window focus + window.dispatchEvent(new Event('visibilitychange')) + + // Wait a bit to ensure no refetch happens + await vi.advanceTimersByTimeAsync(10) + + // Should only have 2 states: initial and after fetch + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) + expect(states[1]).toMatchObject({ data: 0, isFetching: false }) + + // Count should still be 0 since no refetch occurred + expect(count).toBe(1) + }), + ) + + it( + 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => count++, + staleTime: 0, + refetchOnWindowFocus: () => false, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the initial fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + + // Simulate window focus + window.dispatchEvent(new Event('visibilitychange')) + + // Wait a bit to ensure no refetch happens + await vi.advanceTimersByTimeAsync(10) + + // Should only have 2 states: initial and after fetch + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) + expect(states[1]).toMatchObject({ data: 0, isFetching: false }) + + // Count should still be 0 since no refetch occurred + expect(count).toBe(1) + }), + ) + + it( + 'should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', + withEffectRoot(async () => { + const key = queryKey() + const states: Array> = [] + let count = 0 + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => count++, + staleTime: Infinity, + refetchOnWindowFocus: true, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the initial fetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe(0) + + // Simulate window focus + window.dispatchEvent(new Event('visibilitychange')) + + // Wait a bit to ensure no refetch happens + await vi.advanceTimersByTimeAsync(10) + + // Should only have 2 states: initial and after fetch + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) + expect(states[1]).toMatchObject({ data: 0, isFetching: false }) + + // Count should still be 0 since no refetch occurred + expect(count).toBe(1) + }), + ) + + it('should refetch fresh query when refetchOnMount is set to always', async () => { + const key = queryKey() + const states: Array> = [] + + // Prefetch the query + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => 'prefetched', + }) + + await withEffectRoot(async () => { + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => 'data', + refetchOnMount: 'always', + staleTime: Infinity, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the refetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('data') + + // Should have 2 states: initial (with prefetched data) and after refetch + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ + data: 'prefetched', + isStale: false, + isFetching: true, + }) + expect(states[1]).toMatchObject({ + data: 'data', + isStale: false, + isFetching: false, + }) + })() + }) + + it('should refetch stale query when refetchOnMount is set to true', async () => { + const key = queryKey() + const states: Array> = [] + + // Prefetch the query + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => 'prefetched', + }) + + await withEffectRoot(async () => { + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => 'data', + refetchOnMount: true, + staleTime: 0, + }), + () => queryClient, + ) + + $effect(() => { + states.push({ ...query }) + }) + + // Wait for the refetch to complete + await vi.advanceTimersByTimeAsync(0) + expect(query.data).toBe('data') + + // Should have 2 states: initial (with prefetched data) and after refetch + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ + data: 'prefetched', + isStale: true, + isFetching: true, + }) + expect(states[1]).toMatchObject({ + data: 'data', + isStale: true, + isFetching: false, + }) + })() + }) + + it( + 'should set status to error if queryFn throws', + withEffectRoot(async () => { + const key = queryKey() + const consoleMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.reject(new Error('Error test')), + retry: false, + }), + () => queryClient, + ) + + await vi.advanceTimersByTimeAsync(0) + expect(query.status).toBe('error') + expect(query.error?.message).toBe('Error test') + + consoleMock.mockRestore() + }), + ) + + it( + 'should set status to error instead of throwing when error should not be thrown', + withEffectRoot(async () => { + const key = queryKey() + + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.reject(new Error('Local Error')), + retry: false, + throwOnError: (err) => err.message !== 'Local Error', + }), + () => queryClient, + ) + + await vi.advanceTimersByTimeAsync(0) + expect(query.status).toBe('error') + expect(query.error?.message).toBe('Local Error') + }), + ) + + it( + 'should support changing provided query client', + withEffectRoot(() => { + const queryClient1 = new QueryClient() + const queryClient2 = new QueryClient() + + let currentClient = $state(queryClient1) + + const key = queryKey() + + createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('prefetched'), + }), + () => currentClient, + ) + + expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() + + currentClient = queryClient2 + flushSync() + + expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined() + }), + ) + it('should not fetch for the duration of the restoring period when isRestoring is true', async () => { const queryFn = vi.fn(() => sleep(10).then(() => 'data')) diff --git a/packages/svelte-query/tests/createQuery.test-d.ts b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts similarity index 98% rename from packages/svelte-query/tests/createQuery.test-d.ts rename to packages/svelte-query/tests/createQuery/createQuery.test-d.ts index 55115db666..8b6590b334 100644 --- a/packages/svelte-query/tests/createQuery.test-d.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' -import { createQuery, queryOptions } from '../src/index.js' +import { createQuery, queryOptions } from '../../src/index.js' describe('createQuery', () => { describe('initialData', () => {