diff --git a/packages/svelte-query/tests/createQueries.svelte.test.ts b/packages/svelte-query/tests/createQueries.svelte.test.ts deleted file mode 100644 index c648942483c..00000000000 --- a/packages/svelte-query/tests/createQueries.svelte.test.ts +++ /dev/null @@ -1,934 +0,0 @@ -import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' -import { QueryClient, createQueries } from '../src/index.js' -import { promiseWithResolvers, withEffectRoot } from './utils.svelte.js' -import type { - CreateQueryOptions, - CreateQueryResult, - QueryFunction, - QueryFunctionContext, - QueryKey, - skipToken, -} from '../src/index.js' - -describe('createQueries', () => { - const queryClient = new QueryClient() - - afterEach(() => { - queryClient.clear() - }) - - it( - 'should return the correct states', - withEffectRoot(async () => { - const key1 = ['test-1'] - const key2 = ['test-2'] - const results: Array> = [] - const { promise: promise1, resolve: resolve1 } = promiseWithResolvers() - const { promise: promise2, resolve: resolve2 } = promiseWithResolvers() - - const result = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => promise1, - }, - { - queryKey: key2, - queryFn: () => promise2, - }, - ], - }), - () => queryClient, - ) - - $effect(() => { - results.push([{ ...result[0] }, { ...result[1] }]) - }) - - resolve1(1) - - await vi.waitFor(() => expect(result[0].data).toBe(1)) - - resolve2(2) - await vi.waitFor(() => expect(result[1].data).toBe(2)) - - expect(results.length).toBe(3) - expect(results[0]).toMatchObject([ - { data: undefined }, - { data: undefined }, - ]) - expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) - expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) - }), - ) - - it( - 'handles type parameter - tuple of tuples', - withEffectRoot(() => { - const key1 = ['test-key-1'] - const key2 = ['test-key-2'] - const key3 = ['test-key-3'] - - const result1 = createQueries< - [[number], [string], [Array, boolean]] - >( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result1[0]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result1[1]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result1[2]).toEqualTypeOf< - CreateQueryResult, boolean> - >() - expectTypeOf(result1[0].data).toEqualTypeOf() - expectTypeOf(result1[1].data).toEqualTypeOf() - expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() - expectTypeOf(result1[2].error).toEqualTypeOf() - - // TData (3rd element) takes precedence over TQueryFnData (1st element) - const result2 = createQueries< - [[string, unknown, string], [string, unknown, number]] - >( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return parseInt(a) - }, - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result2[0]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result2[1]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result2[0].data).toEqualTypeOf() - expectTypeOf(result2[1].data).toEqualTypeOf() - - // types should be enforced - createQueries<[[string, unknown, string], [string, boolean, number]]>( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return parseInt(a) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - ], - }), - () => queryClient, - ) - - // field names should be enforced - createQueries<[[string]]>( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - }, - ], - }), - () => queryClient, - ) - }), - ) - - it( - 'handles type parameter - tuple of objects', - withEffectRoot(() => { - const key1 = ['test-key-1'] - const key2 = ['test-key-2'] - const key3 = ['test-key-3'] - - const result1 = createQueries< - [ - { queryFnData: number }, - { queryFnData: string }, - { queryFnData: Array; error: boolean }, - ] - >( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result1[0]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result1[1]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result1[2]).toEqualTypeOf< - CreateQueryResult, boolean> - >() - expectTypeOf(result1[0].data).toEqualTypeOf() - expectTypeOf(result1[1].data).toEqualTypeOf() - expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() - expectTypeOf(result1[2].error).toEqualTypeOf() - - // TData (data prop) takes precedence over TQueryFnData (queryFnData prop) - const result2 = createQueries< - [ - { queryFnData: string; data: string }, - { queryFnData: string; data: number }, - ] - >( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return parseInt(a) - }, - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result2[0]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result2[1]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result2[0].data).toEqualTypeOf() - expectTypeOf(result2[1].data).toEqualTypeOf() - - // can pass only TData (data prop) although TQueryFnData will be left unknown - const result3 = createQueries<[{ data: string }, { data: number }]>( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a as string - }, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a as number - }, - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result3[0]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result3[1]).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(result3[0].data).toEqualTypeOf() - expectTypeOf(result3[1].data).toEqualTypeOf() - - // types should be enforced - createQueries< - [ - { queryFnData: string; data: string }, - { queryFnData: string; data: number; error: boolean }, - ] - >( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return parseInt(a) - }, - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - ], - }), - () => queryClient, - ) - - // field names should be enforced - createQueries<[{ queryFnData: string }]>( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - }, - ], - }), - () => queryClient, - ) - }), - ) - - it( - 'handles array literal without type parameter to infer result type', - withEffectRoot(() => { - const key1 = ['test-key-1'] - const key2 = ['test-key-2'] - const key3 = ['test-key-3'] - const key4 = ['test-key-4'] - - // Array.map preserves TQueryFnData - const result1 = createQueries( - () => ({ - queries: Array(50).map((_, i) => ({ - queryKey: ['key', i] as const, - queryFn: () => i + 10, - })), - }), - () => queryClient, - ) - - expectTypeOf(result1).toEqualTypeOf< - Array> - >() - if (result1[0]) { - expectTypeOf(result1[0].data).toEqualTypeOf() - } - - // Array.map preserves TData - const result2 = createQueries( - () => ({ - queries: Array(50).map((_, i) => ({ - queryKey: ['key', i] as const, - queryFn: () => i + 10, - select: (data: number) => data.toString(), - })), - }), - () => queryClient, - ) - - expectTypeOf(result2).toEqualTypeOf< - Array> - >() - - const result3 = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - select: () => 123, - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result3[0]).toEqualTypeOf>() - expectTypeOf(result3[1]).toEqualTypeOf>() - expectTypeOf(result3[2]).toEqualTypeOf>() - expectTypeOf(result3[0].data).toEqualTypeOf() - expectTypeOf(result3[1].data).toEqualTypeOf() - // select takes precedence over queryFn - expectTypeOf(result3[2].data).toEqualTypeOf() - - // initialData/placeholderData are enforced - createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - placeholderData: 'string', - // @ts-expect-error (initialData: string) - initialData: 123, - }, - { - queryKey: key2, - queryFn: () => 123, - // @ts-expect-error (placeholderData: number) - placeholderData: 'string', - initialData: 123, - }, - ], - }), - () => queryClient, - ) - - // select params are "indirectly" enforced - createQueries( - () => ({ - queries: [ - // unfortunately TS will not suggest the type for you - { - queryKey: key1, - queryFn: () => 'string', - }, - // however you can add a type to the callback - { - queryKey: key2, - queryFn: () => 'string', - }, - // the type you do pass is enforced - { - queryKey: key3, - queryFn: () => 'string', - }, - { - queryKey: key4, - queryFn: () => 'string', - select: (a: string) => parseInt(a), - }, - ], - }), - () => queryClient, - ) - - // callbacks are also indirectly enforced with Array.map - createQueries( - () => ({ - queries: Array(50).map((_, i) => ({ - queryKey: ['key', i] as const, - queryFn: () => i + 10, - select: (data: number) => data.toString(), - })), - }), - () => queryClient, - ) - - // results inference works when all the handlers are defined - const result4 = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key4, - queryFn: () => 'string', - select: (a: string) => parseInt(a), - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result4[0]).toEqualTypeOf>() - expectTypeOf(result4[1]).toEqualTypeOf>() - expectTypeOf(result4[2]).toEqualTypeOf>() - - // handles when queryFn returns a Promise - const result5 = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => Promise.resolve('string'), - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result5[0]).toEqualTypeOf>() - - // Array as const does not throw error - const result6 = createQueries( - () => - ({ - queries: [ - { - queryKey: ['key1'], - queryFn: () => 'string', - }, - { - queryKey: ['key1'], - queryFn: () => 123, - }, - ], - }) as const, - () => queryClient, - ) - - expectTypeOf(result6[0]).toEqualTypeOf>() - expectTypeOf(result6[1]).toEqualTypeOf>() - - // field names should be enforced - array literal - createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - }, - ], - }), - () => queryClient, - ) - - // field names should be enforced - Array.map() result - createQueries( - () => ({ - // @ts-expect-error (invalidField) - queries: Array(10).map(() => ({ - someInvalidField: '', - })), - }), - () => queryClient, - ) - - // supports queryFn using fetch() to return Promise - Array.map() result - createQueries( - () => ({ - queries: Array(50).map((_, i) => ({ - queryKey: ['key', i] as const, - queryFn: () => - fetch('return Promise').then((resp) => resp.json()), - })), - }), - () => queryClient, - ) - - // supports queryFn using fetch() to return Promise - array literal - createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => - fetch('return Promise').then((resp) => resp.json()), - }, - ], - }), - () => queryClient, - ) - }), - ) - - it( - 'handles strongly typed queryFn factories and createQueries wrappers', - withEffectRoot(() => { - // QueryKey + queryFn factory - type QueryKeyA = ['queryA'] - const getQueryKeyA = (): QueryKeyA => ['queryA'] - type GetQueryFunctionA = () => QueryFunction - const getQueryFunctionA: GetQueryFunctionA = () => () => { - return 1 - } - type SelectorA = (data: number) => [number, string] - const getSelectorA = (): SelectorA => (data) => [data, data.toString()] - - type QueryKeyB = ['queryB', string] - const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id] - type GetQueryFunctionB = () => QueryFunction - const getQueryFunctionB: GetQueryFunctionB = () => () => { - return '1' - } - type SelectorB = (data: string) => [string, number] - const getSelectorB = (): SelectorB => (data) => [data, +data] - - // Wrapper with strongly typed array-parameter - function useWrappedQueries< - TQueryFnData, - TError, - TData, - TQueryKey extends QueryKey, - >( - queries: Array< - CreateQueryOptions - >, - ) { - return createQueries( - () => ({ - queries: queries.map( - // no need to type the mapped query - (query) => { - const { queryFn: fn, queryKey: key } = query - expectTypeOf(fn).toEqualTypeOf< - | typeof skipToken - | QueryFunction - | undefined - >() - return { - queryKey: key, - queryFn: fn - ? (ctx: QueryFunctionContext) => { - // eslint-disable-next-line vitest/valid-expect - expectTypeOf(ctx.queryKey) - return ( - fn as QueryFunction - ).call({}, ctx) - } - : undefined, - } - }, - ), - }), - () => queryClient, - ) - } - - const result = createQueries( - () => ({ - queries: [ - { - queryKey: getQueryKeyA(), - queryFn: getQueryFunctionA(), - }, - { - queryKey: getQueryKeyB('id'), - queryFn: getQueryFunctionB(), - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(result[0]).toEqualTypeOf>() - expectTypeOf(result[1]).toEqualTypeOf>() - - const withSelector = createQueries( - () => ({ - queries: [ - { - queryKey: getQueryKeyA(), - queryFn: getQueryFunctionA(), - select: getSelectorA(), - }, - { - queryKey: getQueryKeyB('id'), - queryFn: getQueryFunctionB(), - select: getSelectorB(), - }, - ], - }), - () => queryClient, - ) - - expectTypeOf(withSelector[0]).toEqualTypeOf< - CreateQueryResult<[number, string], Error> - >() - expectTypeOf(withSelector[1]).toEqualTypeOf< - CreateQueryResult<[string, number], Error> - >() - - const withWrappedQueries = useWrappedQueries( - Array(10).map(() => ({ - queryKey: getQueryKeyA(), - queryFn: getQueryFunctionA(), - select: getSelectorA(), - })), - ) - - expectTypeOf(withWrappedQueries).toEqualTypeOf< - Array> - >() - }), - ) - - it( - 'should track results', - withEffectRoot(async () => { - const key1 = ['test-track-results'] - const results: Array> = [] - let count = 0 - - const result = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => Promise.resolve(++count), - }, - ], - }), - () => queryClient, - ) - - $effect(() => { - results.push([result[0]]) - }) - - await vi.waitFor(() => expect(result[0].data).toBe(1)) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject([{ data: undefined }]) - expect(results[1]).toMatchObject([{ data: 1 }]) - - // Trigger refetch - result[0].refetch() - - await vi.waitFor(() => expect(result[0].data).toBe(2)) - - // Only one render for data update, no render for isFetching transition - expect(results.length).toBe(3) - expect(results[2]).toMatchObject([{ data: 2 }]) - }), - ) - - it( - 'should combine queries', - withEffectRoot(async () => { - const key1 = ['test-combine-1'] - const key2 = ['test-combine-2'] - - const { promise: promise1, resolve: resolve1 } = - promiseWithResolvers() - const { promise: promise2, resolve: resolve2 } = - promiseWithResolvers() - - const queries = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => promise1, - }, - { - queryKey: key2, - queryFn: () => promise2, - }, - ], - combine: (results) => { - return { - combined: true, - res: results - .flatMap((res) => (res.data ? [res.data] : [])) - .join(','), - } - }, - }), - () => queryClient, - ) - - // Initially both queries are loading - expect(queries).toEqual({ - combined: true, - res: '', - }) - - // Resolve the first query - resolve1('first result') - await vi.waitFor(() => expect(queries.res).toBe('first result')) - - // Resolve the second query - resolve2('second result') - await vi.waitFor(() => - expect(queries.res).toBe('first result,second result'), - ) - - expect(queries).toEqual({ - combined: true, - res: 'first result,second result', - }) - }), - ) - - it( - 'should track property access through combine function', - withEffectRoot(async () => { - const key1 = ['test-track-combine-1'] - const key2 = ['test-track-combine-2'] - let count = 0 - const results: Array = [] - - const { promise: promise1, resolve: resolve1 } = - promiseWithResolvers() - const { promise: promise2, resolve: resolve2 } = - promiseWithResolvers() - const { promise: promise3, resolve: resolve3 } = - promiseWithResolvers() - const { promise: promise4, resolve: resolve4 } = - promiseWithResolvers() - - const queries = createQueries( - () => ({ - queries: [ - { - queryKey: key1, - queryFn: () => (count === 0 ? promise1 : promise3), - }, - { - queryKey: key2, - queryFn: () => (count === 0 ? promise2 : promise4), - }, - ], - combine: (queryResults) => { - return { - combined: true, - refetch: () => - Promise.all(queryResults.map((res) => res.refetch())), - res: queryResults - .flatMap((res) => (res.data ? [res.data] : [])) - .join(','), - } - }, - }), - () => queryClient, - ) - - $effect(() => { - results.push({ ...queries }) - }) - - // Initially both queries are loading - await vi.waitFor(() => - expect(results[0]).toStrictEqual({ - combined: true, - refetch: expect.any(Function), - res: '', - }), - ) - - // Resolve the first query - resolve1('first result ' + count) - await vi.waitFor(() => expect(queries.res).toBe('first result 0')) - - expect(results[1]).toStrictEqual({ - combined: true, - refetch: expect.any(Function), - res: 'first result 0', - }) - - // Resolve the second query - resolve2('second result ' + count) - await vi.waitFor(() => - expect(queries.res).toBe('first result 0,second result 0'), - ) - - expect(results[2]).toStrictEqual({ - combined: true, - refetch: expect.any(Function), - res: 'first result 0,second result 0', - }) - - // Increment count and refetch - count++ - queries.refetch() - - // Resolve the refetched queries - resolve3('first result ' + count) - resolve4('second result ' + count) - - await vi.waitFor(() => - expect(queries.res).toBe('first result 1,second result 1'), - ) - - const length = results.length - expect(results.at(-1)).toStrictEqual({ - combined: true, - refetch: expect.any(Function), - res: 'first result 1,second result 1', - }) - - // Refetch again but with the same data - await queries.refetch() - - // No further re-render because data didn't change - expect(results.length).toBe(length) - }), - ) -}) diff --git a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts index dfff3892fb8..69d2cf845ca 100644 --- a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts +++ b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts @@ -1,18 +1,943 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { render } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' +import { QueryClient, createQueries } from '../../src/index.js' +import { promiseWithResolvers, withEffectRoot } from '../utils.svelte.js' import IsRestoringExample from './IsRestoringExample.svelte' +import type { + CreateQueryOptions, + CreateQueryResult, + QueryFunction, + QueryFunctionContext, + QueryKey, + skipToken, +} from '../../src/index.js' describe('createQueries', () => { - beforeEach(() => { - vi.useFakeTimers() - }) + const queryClient = new QueryClient() afterEach(() => { - vi.useRealTimers() + queryClient.clear() }) + it( + 'should return the correct states', + withEffectRoot(async () => { + const key1 = ['test-1'] + const key2 = ['test-2'] + const results: Array> = [] + const { promise: promise1, resolve: resolve1 } = promiseWithResolvers() + const { promise: promise2, resolve: resolve2 } = promiseWithResolvers() + + const result = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => promise1, + }, + { + queryKey: key2, + queryFn: () => promise2, + }, + ], + }), + () => queryClient, + ) + + $effect(() => { + results.push([{ ...result[0] }, { ...result[1] }]) + }) + + resolve1(1) + + await vi.waitFor(() => expect(result[0].data).toBe(1)) + + resolve2(2) + await vi.waitFor(() => expect(result[1].data).toBe(2)) + + expect(results.length).toBe(3) + expect(results[0]).toMatchObject([ + { data: undefined }, + { data: undefined }, + ]) + expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) + expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) + }), + ) + + it( + 'handles type parameter - tuple of tuples', + withEffectRoot(() => { + const key1 = ['test-key-1'] + const key2 = ['test-key-2'] + const key3 = ['test-key-3'] + + const result1 = createQueries< + [[number], [string], [Array, boolean]] + >( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result1[0]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result1[2]).toEqualTypeOf< + CreateQueryResult, boolean> + >() + expectTypeOf(result1[0].data).toEqualTypeOf() + expectTypeOf(result1[1].data).toEqualTypeOf() + expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() + expectTypeOf(result1[2].error).toEqualTypeOf() + + // TData (3rd element) takes precedence over TQueryFnData (1st element) + const result2 = createQueries< + [[string, unknown, string], [string, unknown, number]] + >( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return parseInt(a) + }, + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result2[0]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result2[0].data).toEqualTypeOf() + expectTypeOf(result2[1].data).toEqualTypeOf() + + // types should be enforced + createQueries<[[string, unknown, string], [string, boolean, number]]>( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return parseInt(a) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + ], + }), + () => queryClient, + ) + + // field names should be enforced + createQueries<[[string]]>( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + }, + ], + }), + () => queryClient, + ) + }), + ) + + it( + 'handles type parameter - tuple of objects', + withEffectRoot(() => { + const key1 = ['test-key-1'] + const key2 = ['test-key-2'] + const key3 = ['test-key-3'] + + const result1 = createQueries< + [ + { queryFnData: number }, + { queryFnData: string }, + { queryFnData: Array; error: boolean }, + ] + >( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result1[0]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result1[2]).toEqualTypeOf< + CreateQueryResult, boolean> + >() + expectTypeOf(result1[0].data).toEqualTypeOf() + expectTypeOf(result1[1].data).toEqualTypeOf() + expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() + expectTypeOf(result1[2].error).toEqualTypeOf() + + // TData (data prop) takes precedence over TQueryFnData (queryFnData prop) + const result2 = createQueries< + [ + { queryFnData: string; data: string }, + { queryFnData: string; data: number }, + ] + >( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return parseInt(a) + }, + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result2[0]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result2[0].data).toEqualTypeOf() + expectTypeOf(result2[1].data).toEqualTypeOf() + + // can pass only TData (data prop) although TQueryFnData will be left unknown + const result3 = createQueries<[{ data: string }, { data: number }]>( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a as string + }, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a as number + }, + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result3[0]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result3[1]).toEqualTypeOf< + CreateQueryResult + >() + expectTypeOf(result3[0].data).toEqualTypeOf() + expectTypeOf(result3[1].data).toEqualTypeOf() + + // types should be enforced + createQueries< + [ + { queryFnData: string; data: string }, + { queryFnData: string; data: number; error: boolean }, + ] + >( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return parseInt(a) + }, + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + ], + }), + () => queryClient, + ) + + // field names should be enforced + createQueries<[{ queryFnData: string }]>( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + }, + ], + }), + () => queryClient, + ) + }), + ) + + it( + 'handles array literal without type parameter to infer result type', + withEffectRoot(() => { + const key1 = ['test-key-1'] + const key2 = ['test-key-2'] + const key3 = ['test-key-3'] + const key4 = ['test-key-4'] + + // Array.map preserves TQueryFnData + const result1 = createQueries( + () => ({ + queries: Array(50).map((_, i) => ({ + queryKey: ['key', i] as const, + queryFn: () => i + 10, + })), + }), + () => queryClient, + ) + + expectTypeOf(result1).toEqualTypeOf< + Array> + >() + if (result1[0]) { + expectTypeOf(result1[0].data).toEqualTypeOf() + } + + // Array.map preserves TData + const result2 = createQueries( + () => ({ + queries: Array(50).map((_, i) => ({ + queryKey: ['key', i] as const, + queryFn: () => i + 10, + select: (data: number) => data.toString(), + })), + }), + () => queryClient, + ) + + expectTypeOf(result2).toEqualTypeOf< + Array> + >() + + const result3 = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + select: () => 123, + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result3[0]).toEqualTypeOf>() + expectTypeOf(result3[1]).toEqualTypeOf>() + expectTypeOf(result3[2]).toEqualTypeOf>() + expectTypeOf(result3[0].data).toEqualTypeOf() + expectTypeOf(result3[1].data).toEqualTypeOf() + // select takes precedence over queryFn + expectTypeOf(result3[2].data).toEqualTypeOf() + + // initialData/placeholderData are enforced + createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + placeholderData: 'string', + // @ts-expect-error (initialData: string) + initialData: 123, + }, + { + queryKey: key2, + queryFn: () => 123, + // @ts-expect-error (placeholderData: number) + placeholderData: 'string', + initialData: 123, + }, + ], + }), + () => queryClient, + ) + + // select params are "indirectly" enforced + createQueries( + () => ({ + queries: [ + // unfortunately TS will not suggest the type for you + { + queryKey: key1, + queryFn: () => 'string', + }, + // however you can add a type to the callback + { + queryKey: key2, + queryFn: () => 'string', + }, + // the type you do pass is enforced + { + queryKey: key3, + queryFn: () => 'string', + }, + { + queryKey: key4, + queryFn: () => 'string', + select: (a: string) => parseInt(a), + }, + ], + }), + () => queryClient, + ) + + // callbacks are also indirectly enforced with Array.map + createQueries( + () => ({ + queries: Array(50).map((_, i) => ({ + queryKey: ['key', i] as const, + queryFn: () => i + 10, + select: (data: number) => data.toString(), + })), + }), + () => queryClient, + ) + + // results inference works when all the handlers are defined + const result4 = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key4, + queryFn: () => 'string', + select: (a: string) => parseInt(a), + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result4[0]).toEqualTypeOf>() + expectTypeOf(result4[1]).toEqualTypeOf>() + expectTypeOf(result4[2]).toEqualTypeOf>() + + // handles when queryFn returns a Promise + const result5 = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => Promise.resolve('string'), + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result5[0]).toEqualTypeOf>() + + // Array as const does not throw error + const result6 = createQueries( + () => + ({ + queries: [ + { + queryKey: ['key1'], + queryFn: () => 'string', + }, + { + queryKey: ['key1'], + queryFn: () => 123, + }, + ], + }) as const, + () => queryClient, + ) + + expectTypeOf(result6[0]).toEqualTypeOf>() + expectTypeOf(result6[1]).toEqualTypeOf>() + + // field names should be enforced - array literal + createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + }, + ], + }), + () => queryClient, + ) + + // field names should be enforced - Array.map() result + createQueries( + () => ({ + // @ts-expect-error (invalidField) + queries: Array(10).map(() => ({ + someInvalidField: '', + })), + }), + () => queryClient, + ) + + // supports queryFn using fetch() to return Promise - Array.map() result + createQueries( + () => ({ + queries: Array(50).map((_, i) => ({ + queryKey: ['key', i] as const, + queryFn: () => + fetch('return Promise').then((resp) => resp.json()), + })), + }), + () => queryClient, + ) + + // supports queryFn using fetch() to return Promise - array literal + createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => + fetch('return Promise').then((resp) => resp.json()), + }, + ], + }), + () => queryClient, + ) + }), + ) + + it( + 'handles strongly typed queryFn factories and createQueries wrappers', + withEffectRoot(() => { + // QueryKey + queryFn factory + type QueryKeyA = ['queryA'] + const getQueryKeyA = (): QueryKeyA => ['queryA'] + type GetQueryFunctionA = () => QueryFunction + const getQueryFunctionA: GetQueryFunctionA = () => () => { + return 1 + } + type SelectorA = (data: number) => [number, string] + const getSelectorA = (): SelectorA => (data) => [data, data.toString()] + + type QueryKeyB = ['queryB', string] + const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id] + type GetQueryFunctionB = () => QueryFunction + const getQueryFunctionB: GetQueryFunctionB = () => () => { + return '1' + } + type SelectorB = (data: string) => [string, number] + const getSelectorB = (): SelectorB => (data) => [data, +data] + + // Wrapper with strongly typed array-parameter + function useWrappedQueries< + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey, + >( + queries: Array< + CreateQueryOptions + >, + ) { + return createQueries( + () => ({ + queries: queries.map( + // no need to type the mapped query + (query) => { + const { queryFn: fn, queryKey: key } = query + expectTypeOf(fn).toEqualTypeOf< + | typeof skipToken + | QueryFunction + | undefined + >() + return { + queryKey: key, + queryFn: fn + ? (ctx: QueryFunctionContext) => { + // eslint-disable-next-line vitest/valid-expect + expectTypeOf(ctx.queryKey) + return ( + fn as QueryFunction + ).call({}, ctx) + } + : undefined, + } + }, + ), + }), + () => queryClient, + ) + } + + const result = createQueries( + () => ({ + queries: [ + { + queryKey: getQueryKeyA(), + queryFn: getQueryFunctionA(), + }, + { + queryKey: getQueryKeyB('id'), + queryFn: getQueryFunctionB(), + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(result[0]).toEqualTypeOf>() + expectTypeOf(result[1]).toEqualTypeOf>() + + const withSelector = createQueries( + () => ({ + queries: [ + { + queryKey: getQueryKeyA(), + queryFn: getQueryFunctionA(), + select: getSelectorA(), + }, + { + queryKey: getQueryKeyB('id'), + queryFn: getQueryFunctionB(), + select: getSelectorB(), + }, + ], + }), + () => queryClient, + ) + + expectTypeOf(withSelector[0]).toEqualTypeOf< + CreateQueryResult<[number, string], Error> + >() + expectTypeOf(withSelector[1]).toEqualTypeOf< + CreateQueryResult<[string, number], Error> + >() + + const withWrappedQueries = useWrappedQueries( + Array(10).map(() => ({ + queryKey: getQueryKeyA(), + queryFn: getQueryFunctionA(), + select: getSelectorA(), + })), + ) + + expectTypeOf(withWrappedQueries).toEqualTypeOf< + Array> + >() + }), + ) + + it( + 'should track results', + withEffectRoot(async () => { + const key1 = ['test-track-results'] + const results: Array> = [] + let count = 0 + + const result = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => Promise.resolve(++count), + }, + ], + }), + () => queryClient, + ) + + $effect(() => { + results.push([result[0]]) + }) + + await vi.waitFor(() => expect(result[0].data).toBe(1)) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject([{ data: undefined }]) + expect(results[1]).toMatchObject([{ data: 1 }]) + + // Trigger refetch + result[0].refetch() + + await vi.waitFor(() => expect(result[0].data).toBe(2)) + + // Only one render for data update, no render for isFetching transition + expect(results.length).toBe(3) + expect(results[2]).toMatchObject([{ data: 2 }]) + }), + ) + + it( + 'should combine queries', + withEffectRoot(async () => { + const key1 = ['test-combine-1'] + const key2 = ['test-combine-2'] + + const { promise: promise1, resolve: resolve1 } = + promiseWithResolvers() + const { promise: promise2, resolve: resolve2 } = + promiseWithResolvers() + + const queries = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => promise1, + }, + { + queryKey: key2, + queryFn: () => promise2, + }, + ], + combine: (results) => { + return { + combined: true, + res: results + .flatMap((res) => (res.data ? [res.data] : [])) + .join(','), + } + }, + }), + () => queryClient, + ) + + // Initially both queries are loading + expect(queries).toEqual({ + combined: true, + res: '', + }) + + // Resolve the first query + resolve1('first result') + await vi.waitFor(() => expect(queries.res).toBe('first result')) + + // Resolve the second query + resolve2('second result') + await vi.waitFor(() => + expect(queries.res).toBe('first result,second result'), + ) + + expect(queries).toEqual({ + combined: true, + res: 'first result,second result', + }) + }), + ) + + it( + 'should track property access through combine function', + withEffectRoot(async () => { + const key1 = ['test-track-combine-1'] + const key2 = ['test-track-combine-2'] + let count = 0 + const results: Array = [] + + const { promise: promise1, resolve: resolve1 } = + promiseWithResolvers() + const { promise: promise2, resolve: resolve2 } = + promiseWithResolvers() + const { promise: promise3, resolve: resolve3 } = + promiseWithResolvers() + const { promise: promise4, resolve: resolve4 } = + promiseWithResolvers() + + const queries = createQueries( + () => ({ + queries: [ + { + queryKey: key1, + queryFn: () => (count === 0 ? promise1 : promise3), + }, + { + queryKey: key2, + queryFn: () => (count === 0 ? promise2 : promise4), + }, + ], + combine: (queryResults) => { + return { + combined: true, + refetch: () => + Promise.all(queryResults.map((res) => res.refetch())), + res: queryResults + .flatMap((res) => (res.data ? [res.data] : [])) + .join(','), + } + }, + }), + () => queryClient, + ) + + $effect(() => { + results.push({ ...queries }) + }) + + // Initially both queries are loading + await vi.waitFor(() => + expect(results[0]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: '', + }), + ) + + // Resolve the first query + resolve1('first result ' + count) + await vi.waitFor(() => expect(queries.res).toBe('first result 0')) + + expect(results[1]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 0', + }) + + // Resolve the second query + resolve2('second result ' + count) + await vi.waitFor(() => + expect(queries.res).toBe('first result 0,second result 0'), + ) + + expect(results[2]).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 0,second result 0', + }) + + // Increment count and refetch + count++ + queries.refetch() + + // Resolve the refetched queries + resolve3('first result ' + count) + resolve4('second result ' + count) + + await vi.waitFor(() => + expect(queries.res).toBe('first result 1,second result 1'), + ) + + const length = results.length + expect(results.at(-1)).toStrictEqual({ + combined: true, + refetch: expect.any(Function), + res: 'first result 1,second result 1', + }) + + // Refetch again but with the same data + await queries.refetch() + + // No further re-render because data didn't change + expect(results.length).toBe(length) + }), + ) + it('should not fetch for the duration of the restoring period when isRestoring is true', async () => { + vi.useFakeTimers() + const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1')) const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2')) @@ -41,5 +966,7 @@ describe('createQueries', () => { expect(rendered.getByTestId('data2')).toHaveTextContent('undefined') expect(queryFn1).toHaveBeenCalledTimes(0) expect(queryFn2).toHaveBeenCalledTimes(0) + + vi.useRealTimers() }) }) diff --git a/packages/svelte-query/tests/createQueries.test-d.ts b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts similarity index 86% rename from packages/svelte-query/tests/createQueries.test-d.ts rename to packages/svelte-query/tests/createQueries/createQueries.test-d.ts index 016f5a53a5b..0783afe4e9b 100644 --- a/packages/svelte-query/tests/createQueries.test-d.ts +++ b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' -import { createQueries, queryOptions } from '../src/index.js' -import type { CreateQueryResult } from '../src/index.js' +import { createQueries, queryOptions } from '../../src/index.js' +import type { CreateQueryResult } from '../../src/index.js' describe('createQueries', () => { it('should return correct data for dynamic queries with mixed result types', () => {