snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
225
packages/solid-query/src/__tests__/QueryClientProvider.test.tsx
Normal file
225
packages/solid-query/src/__tests__/QueryClientProvider.test.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render } from '@solidjs/testing-library'
|
||||
import { QueryCache } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { createMemo, createRoot } from 'solid-js'
|
||||
import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '..'
|
||||
import { useQueryClientResolver } from '../QueryClientProvider'
|
||||
|
||||
describe('QueryClientProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('sets a specific cache for all queries to use', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryCache = new QueryCache()
|
||||
const queryClient = new QueryClient({ queryCache })
|
||||
|
||||
function Page() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.state.data).toBe('test')
|
||||
})
|
||||
|
||||
it('allows multiple caches to be partitioned', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const queryCache1 = new QueryCache()
|
||||
const queryCache2 = new QueryCache()
|
||||
|
||||
const queryClient1 = new QueryClient({ queryCache: queryCache1 })
|
||||
const queryClient2 = new QueryClient({ queryCache: queryCache2 })
|
||||
|
||||
function Page1() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 'test1'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function Page2() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(10).then(() => 'test2'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient1}>
|
||||
<Page1 />
|
||||
</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient2}>
|
||||
<Page2 />
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test1')).toBeInTheDocument()
|
||||
expect(rendered.getByText('test2')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1')
|
||||
expect(queryCache1.find({ queryKey: key2 })).toBeUndefined()
|
||||
expect(queryCache2.find({ queryKey: key1 })).toBeUndefined()
|
||||
expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2')
|
||||
})
|
||||
|
||||
it("uses defaultOptions for queries when they don't provide their own config", async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryCache = new QueryCache()
|
||||
const queryClient = new QueryClient({
|
||||
queryCache,
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity)
|
||||
})
|
||||
|
||||
describe('useQueryClient', () => {
|
||||
it('should throw an error if no query client has been set', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
useQueryClient()
|
||||
return null
|
||||
}
|
||||
|
||||
expect(() => render(() => <Page />)).toThrow(
|
||||
'No QueryClient set, use QueryClientProvider to set one',
|
||||
)
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw an error if user provides custom query client', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const client = new QueryClient()
|
||||
useQueryClient(client)
|
||||
return null
|
||||
}
|
||||
|
||||
render(() => <Page />)
|
||||
expect(consoleMock).not.toHaveBeenCalled()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('creates a query client resolver that is safe to call in reactive callbacks', () => {
|
||||
const queryClient = new QueryClient()
|
||||
let resolveClient!: () => QueryClient
|
||||
|
||||
function Page() {
|
||||
resolveClient = useQueryClientResolver()
|
||||
return null
|
||||
}
|
||||
|
||||
render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
createRoot((dispose) => {
|
||||
const client = createMemo(() => resolveClient())
|
||||
|
||||
expect(client()).toBe(queryClient)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it('defers missing provider errors until a resolver is called', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
let resolveClient!: () => QueryClient
|
||||
|
||||
function Page() {
|
||||
resolveClient = useQueryClientResolver()
|
||||
return null
|
||||
}
|
||||
|
||||
expect(() => render(() => <Page />)).not.toThrow()
|
||||
|
||||
expect(() =>
|
||||
createRoot((dispose) => {
|
||||
const client = createMemo(() => resolveClient())
|
||||
client()
|
||||
dispose()
|
||||
}),
|
||||
).toThrow('No QueryClient set, use QueryClientProvider to set one')
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from '../infiniteQueryOptions'
|
||||
|
||||
// Regression test for exported infiniteQueryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedInfiniteQueryOptions = infiniteQueryOptions({
|
||||
queryKey: ['invalid'],
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should work when passed to infiniteQuery', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with select', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
select: (data) => data.pages,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Array<{ wow: boolean }>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with enabled: false', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with skipToken', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<unknown, number>>()
|
||||
})
|
||||
|
||||
it('should infer defined types', () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: {
|
||||
pageParams: [undefined],
|
||||
pages: [{ wow: true }],
|
||||
},
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expectTypeOf(useInfiniteQuery(() => options).data).toEqualTypeOf<
|
||||
InfiniteData<{ wow: boolean }, unknown>
|
||||
>()
|
||||
|
||||
expectTypeOf(options).toExtend<
|
||||
ReturnType<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
{ wow: boolean },
|
||||
Error,
|
||||
InfiniteData<{ wow: boolean }, unknown>,
|
||||
Array<string>,
|
||||
number | undefined
|
||||
>
|
||||
>
|
||||
>()
|
||||
|
||||
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
|
||||
InfiniteData<{ wow: boolean }>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work without defined types', () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => undefined,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expectTypeOf(() => useInfiniteQuery(() => options).data).toEqualTypeOf<
|
||||
() => InfiniteData<{ wow: boolean }, unknown> | undefined
|
||||
>()
|
||||
|
||||
expectTypeOf(options).toExtend<
|
||||
ReturnType<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
{ wow: boolean },
|
||||
Error,
|
||||
InfiniteData<{ wow: boolean }, unknown>,
|
||||
Array<string>,
|
||||
number
|
||||
>
|
||||
>
|
||||
>()
|
||||
|
||||
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
|
||||
InfiniteData<{
|
||||
wow: boolean
|
||||
}>
|
||||
>()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
getNextPageParam: () => null,
|
||||
initialPageParam: null,
|
||||
} as const
|
||||
|
||||
expect(infiniteQueryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
218
packages/solid-query/src/__tests__/mutationOptions.test-d.tsx
Normal file
218
packages/solid-query/src/__tests__/mutationOptions.test-d.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useIsMutating, useMutation, useMutationState } from '..'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationState,
|
||||
WithRequired,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MutationOptions, UseMutationResult } from '../types'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error this is a good error, because onMutates does not exist!
|
||||
onMutates: 1000,
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for onError callback', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => {
|
||||
throw new Error('fail')
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for variables', () => {
|
||||
mutationOptions<number, DefaultError, { id: string }>({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer result type correctly', () => {
|
||||
mutationOptions<number, DefaultError, void, { name: string }>({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: queryKey(),
|
||||
onMutate: () => {
|
||||
return { name: 'onMutateResult' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer context type correctly', () => {
|
||||
mutationOptions<number>({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should error if mutationFn return type mismatches TData', () => {
|
||||
assertType(
|
||||
mutationOptions<number>({
|
||||
// @ts-expect-error this is a good error, because return type is string, not number
|
||||
mutationFn: async () => Promise.resolve('wrong return'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow mutationKey to be omitted', () => {
|
||||
return mutationOptions({
|
||||
mutationFn: () => Promise.resolve(123),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer all types when not explicitly provided', () => {
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
WithRequired<MutationOptions<number, DefaultError, string>, 'mutationKey'>
|
||||
>()
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
Omit<MutationOptions<number, DefaultError, string>, 'mutationKey'>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should infer types when used with useMutation', () => {
|
||||
const mutation = useMutation(() =>
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
UseMutationResult<string, DefaultError, void, unknown>
|
||||
>()
|
||||
|
||||
useMutation(() =>
|
||||
// should allow when used with useMutation without mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with useIsMutating', () => {
|
||||
const isMutating = useIsMutating(() =>
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<() => number>()
|
||||
|
||||
useIsMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
() =>
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with queryClient.isMutating', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const isMutating = queryClient.isMutating(
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<number>()
|
||||
|
||||
queryClient.isMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with useMutationState', () => {
|
||||
const mutationState = useMutationState(() => ({
|
||||
filters: mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
}))
|
||||
expectTypeOf(mutationState).toEqualTypeOf<
|
||||
() => Array<MutationState<unknown, Error, unknown, unknown>>
|
||||
>()
|
||||
|
||||
useMutationState(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
() => ({
|
||||
filters: mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
511
packages/solid-query/src/__tests__/mutationOptions.test.tsx
Normal file
511
packages/solid-query/src/__tests__/mutationOptions.test.tsx
Normal file
@@ -0,0 +1,511 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { createEffect, createRenderEffect } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useIsMutating, useMutation, useMutationState } from '..'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { MutationState } from '@tanstack/query-core'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[2]).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating(() => ({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}))
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey: mutationOpts.mutationKey, status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(2)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data1')
|
||||
expect(lastSnapshot[1]?.data).toEqual('data2')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data1')
|
||||
})
|
||||
})
|
||||
209
packages/solid-query/src/__tests__/queryOptions.test-d.tsx
Normal file
209
packages/solid-query/src/__tests__/queryOptions.test-d.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import {
|
||||
QueryClient,
|
||||
dataTagErrorSymbol,
|
||||
dataTagSymbol,
|
||||
skipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useQuery } from '../useQuery'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
|
||||
// Regression test for exported queryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedQueryOptions = queryOptions({
|
||||
queryKey: ['invalid'],
|
||||
})
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
assertType(
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('should infer types for callbacks', () => {
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
staleTime: 1000,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should work when passed to useQuery', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const { data } = useQuery(() => options)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should work when passed to query', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should work when passed to query with select', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
})
|
||||
it('should work when passed to query with enabled: false', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should work when passed to query with skipToken', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
it('should work when passed to fetchQuery', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const data = await new QueryClient().fetchQuery(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey even if no promise is returned', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 5,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with unknown if there is no queryFn', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<unknown>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with the default error type', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagErrorSymbol]).toEqualTypeOf<Error>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(tagged)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryState', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const state = queryClient.getQueryState(tagged)
|
||||
expectTypeOf(state?.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type updaterFn when passed to setQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(tagged, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type value when passed to setQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, '5')
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, () => '5')
|
||||
|
||||
const data = queryClient.setQueryData(tagged, 5)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer even if there is a conditional skipToken', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer to unknown if we disable a query with just a skipToken', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
})
|
||||
13
packages/solid-query/src/__tests__/queryOptions.test.tsx
Normal file
13
packages/solid-query/src/__tests__/queryOptions.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
} as const
|
||||
|
||||
expect(queryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
898
packages/solid-query/src/__tests__/suspense.test.tsx
Normal file
898
packages/solid-query/src/__tests__/suspense.test.tsx
Normal file
@@ -0,0 +1,898 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import {
|
||||
ErrorBoundary,
|
||||
Show,
|
||||
Suspense,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
on,
|
||||
} from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useInfiniteQuery, useQuery } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from '..'
|
||||
|
||||
describe("useQuery's in Suspense mode", () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render the correct amount of times in Suspense mode', async () => {
|
||||
const key = queryKey()
|
||||
const states: Array<UseQueryResult<number>> = []
|
||||
|
||||
let count = 0
|
||||
let renders = 0
|
||||
|
||||
function Page() {
|
||||
const [stateKey, setStateKey] = createSignal(key)
|
||||
|
||||
const state = useQuery(() => ({
|
||||
queryKey: stateKey(),
|
||||
queryFn: () => sleep(10).then(() => ++count),
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
states.push({ ...state })
|
||||
})
|
||||
|
||||
createRenderEffect(
|
||||
on([() => ({ ...state }), () => key], () => {
|
||||
renders++
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
|
||||
data: {String(state.data)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
|
||||
expect(renders).toBe(4)
|
||||
expect(states.length).toBe(4)
|
||||
expect(states[1]).toMatchObject({ data: 1, status: 'success' })
|
||||
expect(states[3]).toMatchObject({ data: 2, status: 'success' })
|
||||
})
|
||||
|
||||
it('should return the correct states for a successful infinite query', async () => {
|
||||
const key = queryKey()
|
||||
const states: Array<UseInfiniteQueryResult<InfiniteData<number>>> = []
|
||||
|
||||
function Page() {
|
||||
const [multiplier, setMultiplier] = createSignal(1)
|
||||
const state = useInfiniteQuery(() => ({
|
||||
queryKey: [`${key}_${multiplier()}`],
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(10).then(() => pageParam * multiplier()),
|
||||
initialPageParam: 1,
|
||||
suspense: true,
|
||||
getNextPageParam: (lastPage) => lastPage + 1,
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
states.push({ ...state })
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMultiplier(2)}>next</button>
|
||||
data: {state.data?.pages.join(',')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
// eslint-disable-next-line cspell/spellchecker
|
||||
// TODO(lukemurray): in react this is 1 in solid this is 2 because suspense
|
||||
// occurs on read.
|
||||
expect(states.length).toBe(2)
|
||||
expect(states[1]).toMatchObject({
|
||||
data: { pages: [1], pageParams: [1] },
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByText('next'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
// eslint-disable-next-line cspell/spellchecker
|
||||
// TODO(lukemurray): in react this is 2 and in solid it is 4
|
||||
expect(states.length).toBe(4)
|
||||
expect(states[3]).toMatchObject({
|
||||
data: { pages: [2], pageParams: [1] },
|
||||
status: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call the queryFn twice when used in Suspense mode', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return <>rendered</>
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should remove query instance when component unmounted', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return <>rendered</>
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [show, setShow] = createSignal(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback="loading">{show() && <Page />}</Suspense>
|
||||
<button
|
||||
aria-label="toggle"
|
||||
onClick={() => setShow((prev) => !prev)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
|
||||
expect(queryCache.find({ queryKey: key })).toBeUndefined()
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(1)
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(0)
|
||||
})
|
||||
|
||||
// https://github.com/tannerlinsley/react-query/issues/468
|
||||
it('should reset error state if new component instances are mounted', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
let succeed = false
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retryDelay: 10,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// Suspense only triggers if used in JSX
|
||||
return (
|
||||
<Show when={state.data}>
|
||||
<div>rendered</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<ErrorBoundary
|
||||
fallback={(_err, resetSolid) => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
succeed = true
|
||||
resetSolid()
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should retry fetch if the reset error boundary has been reset', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
let succeed = false
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// Suspense only triggers if used in JSX
|
||||
return (
|
||||
<Show when={state.data}>
|
||||
<div>rendered</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<ErrorBoundary
|
||||
fallback={(_err, resetSolid) => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
resetSolid()
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
succeed = true
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should refetch when re-mounting', async () => {
|
||||
const key = queryKey()
|
||||
let count = 0
|
||||
|
||||
function Component() {
|
||||
const result = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(100).then(() => ++count),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
staleTime: 0,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>data: {result.data}</span>
|
||||
<span>fetching: {result.isFetching ? 'true' : 'false'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [show, setShow] = createSignal(true)
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShow(!show())
|
||||
}}
|
||||
>
|
||||
{show() ? 'hide' : 'show'}
|
||||
</button>
|
||||
<Suspense fallback="loading">{show() && <Component />}</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
|
||||
expect(rendered.getByText('hide')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('hide'))
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('show')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('show'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('fetching: true')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should suspend when switching to a new query', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
function Component(props: { queryKey: Array<string> }) {
|
||||
const result = useQuery(() => ({
|
||||
queryKey: props.queryKey,
|
||||
queryFn: () => sleep(100).then(() => props.queryKey),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return <div>data: {result.data}</div>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [key, setKey] = createSignal(key1)
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setKey(key2)
|
||||
}}
|
||||
>
|
||||
switch
|
||||
</button>
|
||||
<Suspense fallback="loading">
|
||||
<Component queryKey={key()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText(`data: ${key1}`)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('switch'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText(`data: ${key2}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should throw errors to the error boundary by default', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Suspense Error a1x'))),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should not throw errors to the error boundary when throwOnError: false', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Suspense Error a2x'))),
|
||||
retry: false,
|
||||
throwOnError: false,
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should throw errors to the error boundary when a throwOnError function returns true', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Remote Error'))),
|
||||
retry: false,
|
||||
throwOnError: (err) => err.message !== 'Local Error',
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should not throw errors to the error boundary when a throwOnError function returns false', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Local Error'))),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
throwOnError: (err) => err.message !== 'Local Error',
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not call the queryFn when not enabled', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryFn = vi.fn(() => sleep(10).then(() => '23'))
|
||||
|
||||
function Page() {
|
||||
const [enabled, setEnabled] = createSignal(false)
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn,
|
||||
suspense: true,
|
||||
enabled: enabled(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setEnabled(true)}>fire</button>
|
||||
<h1>{result.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
fireEvent.click(rendered.getByRole('button', { name: /fire/i }))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByRole('heading').textContent).toBe('23')
|
||||
expect(queryFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
let succeed = true
|
||||
|
||||
function Page() {
|
||||
const [nonce] = createSignal(0)
|
||||
const queryKeys = [`${key}-${succeed}`]
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: queryKeys,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return nonce()
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button aria-label="fail" onClick={() => queryClient.resetQueries()}>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
// render suspense fallback (Loading...)
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// resolve promise -> render Page (rendered)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change query key
|
||||
succeed = false
|
||||
// reset query -> and throw error
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop when query keys changed', async () => {
|
||||
let succeed = true
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const [key, setKey] = createSignal(0)
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: [`${key()}-${succeed}`],
|
||||
queryFn: async () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button aria-label="fail" onClick={() => setKey((k) => k + 1)}>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
// render suspense fallback (Loading...)
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// resolve promise -> render Page (rendered)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change promise result to error
|
||||
succeed = false
|
||||
// change query key
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop when enabled changed', async () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const queryKeys = '1'
|
||||
const [enabled, setEnabled] = createSignal(false)
|
||||
|
||||
const result = useQuery<string>(() => ({
|
||||
queryKey: [queryKeys],
|
||||
queryFn: () =>
|
||||
sleep(10).then(() =>
|
||||
Promise.reject(new Error('Suspense Error Bingo')),
|
||||
),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
enabled: enabled(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button
|
||||
aria-label="fail"
|
||||
onClick={() => {
|
||||
setEnabled(true)
|
||||
}}
|
||||
>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
// render empty data with 'rendered' when enabled is false
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change enabled to true
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
// render pending fallback
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {
|
||||
const key = queryKey()
|
||||
let state: UseQueryResult<number> | null = null
|
||||
|
||||
let count = 0
|
||||
let renders = 0
|
||||
|
||||
function Page() {
|
||||
state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => ++count),
|
||||
gcTime: 0,
|
||||
}))
|
||||
|
||||
createRenderEffect(
|
||||
on([() => ({ ...state })], () => {
|
||||
renders++
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(state).toMatchObject({
|
||||
data: 1,
|
||||
status: 'success',
|
||||
})
|
||||
expect(renders).toBe(2)
|
||||
expect(rendered.queryByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
66
packages/solid-query/src/__tests__/transition.test.tsx
Normal file
66
packages/solid-query/src/__tests__/transition.test.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { Show, Suspense, createSignal, startTransition } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useQuery } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
|
||||
describe("useQuery's in Suspense mode with transitions", () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render the content when the transition is done', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Suspended() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => true),
|
||||
}))
|
||||
return <Show when={state.data}>Message</Show>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [showSignal, setShowSignal] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
aria-label="toggle"
|
||||
onClick={() =>
|
||||
startTransition(() => setShowSignal((value) => !value))
|
||||
}
|
||||
>
|
||||
{showSignal() ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
<Suspense fallback="Loading">
|
||||
<Show when={showSignal()}>
|
||||
<Suspended />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('Show')).toBeInTheDocument()
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('Message')).toBeInTheDocument()
|
||||
// verify that the button also updated. See https://github.com/solidjs/solid/issues/1249
|
||||
expect(rendered.getByText('Hide')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
210
packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
210
packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('useInfiniteQuery', () => {
|
||||
describe('pageParam', () => {
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext', () => {
|
||||
useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('direction should be passed to queryFn of useInfiniteQuery', () => {
|
||||
useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ direction }) => {
|
||||
expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.prefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
initialData: { pages: [5], pageParams: [1] },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('select', () => {
|
||||
it('should still return paginated data if no select result', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('should be able to transform data to arbitrary result', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, number>>()
|
||||
return 'selected' as const
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextPageParam / getPreviousPageParam', () => {
|
||||
it('should get typed params', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return String(pageParam)
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (
|
||||
lastPage,
|
||||
allPages,
|
||||
lastPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(lastPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(lastPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (
|
||||
firstPage,
|
||||
allPages,
|
||||
firstPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(firstPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(firstPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('error booleans', () => {
|
||||
it('should not be permanently `false`', () => {
|
||||
const {
|
||||
isFetchNextPageError,
|
||||
isFetchPreviousPageError,
|
||||
isLoadingError,
|
||||
isRefetchError,
|
||||
} = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(isFetchNextPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isFetchPreviousPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isLoadingError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isRefetchError).toEqualTypeOf<boolean>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryClient', () => {
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const infiniteQuery = useInfiniteQuery(
|
||||
() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
})
|
||||
1983
packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx
Normal file
1983
packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
292
packages/solid-query/src/__tests__/useIsFetching.test.tsx
Normal file
292
packages/solid-query/src/__tests__/useIsFetching.test.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useIsFetching, useQuery } from '..'
|
||||
import { renderWithClient, setActTimeout } from './utils'
|
||||
|
||||
describe('useIsFetching', () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// See https://github.com/tannerlinsley/react-query/issues/105
|
||||
it('should update as queries start and stop fetching', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function IsFetching() {
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
return <div>isFetching: {isFetching()}</div>
|
||||
}
|
||||
|
||||
function Query() {
|
||||
const [ready, setReady] = createSignal(false)
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(50).then(() => 'test'),
|
||||
enabled: ready(),
|
||||
}))
|
||||
|
||||
return <button onClick={() => setReady(true)}>setReady</button>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<IsFetching />
|
||||
<Query />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setReady/i }))
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not update state while rendering', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const isFetchingArray: Array<number> = []
|
||||
|
||||
function IsFetching() {
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
createRenderEffect(() => {
|
||||
isFetchingArray.push(isFetching())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function FirstQuery() {
|
||||
useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(150).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function SecondQuery() {
|
||||
useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(200).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [renderSecond, setRenderSecond] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
setActTimeout(() => {
|
||||
setRenderSecond(true)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<IsFetching />
|
||||
<FirstQuery />
|
||||
<Show when={renderSecond()}>
|
||||
<SecondQuery />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// unlike react, Updating renderSecond wont cause a rerender for FirstQuery
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
|
||||
expect(isFetchingArray).toEqual([0, 1, 2, 1, 0])
|
||||
})
|
||||
|
||||
it('should be able to filter', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const isFetchingArray: Array<number> = []
|
||||
|
||||
function One() {
|
||||
useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Two() {
|
||||
useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(20).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [started, setStarted] = createSignal(false)
|
||||
const isFetching = useIsFetching(() => ({
|
||||
queryKey: key1,
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isFetchingArray.push(isFetching())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setStarted(true)}>setStarted</button>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
<Show when={started()}>
|
||||
<>
|
||||
<One />
|
||||
<Two />
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setStarted/i }))
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
// at no point should we have isFetching: 2
|
||||
expect(isFetchingArray).toEqual(expect.not.arrayContaining([2]))
|
||||
})
|
||||
|
||||
it('should show the correct fetching state when mounted after a query', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const customClient = new QueryClient()
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(
|
||||
() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}),
|
||||
() => customClient,
|
||||
)
|
||||
|
||||
const isFetching = useIsFetching(undefined, () => customClient)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page></Page>)
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should resubscribe when a custom queryClient changes', async () => {
|
||||
const queryClient1 = new QueryClient()
|
||||
const queryClient2 = new QueryClient()
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const [client, setClient] = createSignal(queryClient1)
|
||||
const queryCache1 = queryClient1.getQueryCache()
|
||||
const originalSubscribe1 = queryCache1.subscribe.bind(queryCache1)
|
||||
const unsubscribe1 = vi.fn()
|
||||
|
||||
vi.spyOn(queryCache1, 'subscribe').mockImplementation((listener) => {
|
||||
const cleanup = originalSubscribe1(listener)
|
||||
|
||||
return () => {
|
||||
unsubscribe1()
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const isFetching = useIsFetching(undefined, client)
|
||||
|
||||
return <div>isFetching: {isFetching()}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
const firstQuery = queryClient1.query({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(20).then(() => 'test1'),
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
|
||||
setClient(queryClient2)
|
||||
|
||||
expect(unsubscribe1).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
const secondQuery = queryClient2.query({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(20).then(() => 'test2'),
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
await Promise.all([firstQuery, secondQuery])
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
303
packages/solid-query/src/__tests__/useIsMutating.test.tsx
Normal file
303
packages/solid-query/src/__tests__/useIsMutating.test.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
|
||||
import * as QueryCore from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useIsMutating, useMutation } from '..'
|
||||
import { renderWithClient, setActTimeout } from './utils'
|
||||
|
||||
describe('useIsMutating', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Mutations() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(150).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
setActTimeout(() => {
|
||||
mutate2()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<IsMutating />
|
||||
<Mutations />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 2, 1, 0])
|
||||
})
|
||||
|
||||
it('should filter correctly by mutationKey', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating(() => ({ mutationKey: mutationKey1 }))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
mutate2()
|
||||
})
|
||||
|
||||
return <IsMutating />
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// Unlike React, IsMutating Wont re-render twice with mutation2
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should filter correctly by predicate', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating(() => ({
|
||||
predicate: (mutation) =>
|
||||
mutation.options.mutationKey?.[0] === mutationKey1[0],
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
mutate2()
|
||||
})
|
||||
|
||||
return <IsMutating />
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// Again, No unnecessary re-renders like React
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const customClient = new QueryClient()
|
||||
const mutationKey1 = queryKey()
|
||||
|
||||
function Page() {
|
||||
const isMutating = useIsMutating(undefined, () => customClient)
|
||||
const { mutate } = useMutation(
|
||||
() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(20).then(() => 'data'),
|
||||
}),
|
||||
() => customClient,
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
setActTimeout(() => {
|
||||
mutate()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>mutating: {isMutating()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page></Page>)
|
||||
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should resubscribe when a custom queryClient changes', async () => {
|
||||
const queryClient1 = new QueryClient()
|
||||
const queryClient2 = new QueryClient()
|
||||
const [client, setClient] = createSignal(queryClient1)
|
||||
const mutationCache1 = queryClient1.getMutationCache()
|
||||
const originalSubscribe1 = mutationCache1.subscribe.bind(mutationCache1)
|
||||
const unsubscribe1 = vi.fn()
|
||||
|
||||
vi.spyOn(mutationCache1, 'subscribe').mockImplementation((listener) => {
|
||||
const cleanup = originalSubscribe1(listener)
|
||||
|
||||
return () => {
|
||||
unsubscribe1()
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const isMutating = useIsMutating(undefined, client)
|
||||
|
||||
return <div>mutating: {isMutating()}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
const firstMutation = queryClient1.getMutationCache().build(queryClient1, {
|
||||
mutationFn: () => sleep(20).then(() => 'data1'),
|
||||
})
|
||||
const firstMutationPromise = firstMutation.execute(undefined)
|
||||
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
|
||||
setClient(queryClient2)
|
||||
|
||||
expect(unsubscribe1).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
|
||||
const secondMutation = queryClient2.getMutationCache().build(queryClient2, {
|
||||
mutationFn: () => sleep(20).then(() => 'data2'),
|
||||
})
|
||||
const secondMutationPromise = secondMutation.execute(undefined)
|
||||
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
await Promise.all([firstMutationPromise, secondMutationPromise])
|
||||
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line vitest/expect-expect
|
||||
it('should not change state if unmounted', async () => {
|
||||
// We have to mock the MutationCache to not unsubscribe
|
||||
// the listener when the component is unmounted
|
||||
class MutationCacheMock extends QueryCore.MutationCache {
|
||||
subscribe(listener: any) {
|
||||
super.subscribe(listener)
|
||||
return () => void 0
|
||||
}
|
||||
}
|
||||
|
||||
const MutationCacheSpy = vi
|
||||
.spyOn(QueryCore, 'MutationCache')
|
||||
.mockImplementation((fn) => {
|
||||
return new MutationCacheMock(fn)
|
||||
})
|
||||
|
||||
// Create the client after mocking MutationCache so it uses the mock,
|
||||
// not the centralized client from beforeEach
|
||||
const spiedClient = new QueryClient()
|
||||
const mutationKey1 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
useIsMutating()
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [mounted, setMounted] = createSignal(true)
|
||||
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMounted(false)}>unmount</button>
|
||||
<Show when={mounted()}>
|
||||
<IsMutating />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(spiedClient, () => <Page />)
|
||||
|
||||
fireEvent.click(rendered.getByText('unmount'))
|
||||
|
||||
// Should not display the console error
|
||||
// "Warning: Can't perform a React state update on an unmounted component"
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
MutationCacheSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
204
packages/solid-query/src/__tests__/useMutation.test-d.tsx
Normal file
204
packages/solid-query/src/__tests__/useMutation.test-d.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationKey,
|
||||
QueryClient as QueryCoreClient,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseMutationResult } from '../types'
|
||||
|
||||
describe('useMutation', () => {
|
||||
it('should infer TData from mutationFn return type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<DefaultError | null>()
|
||||
})
|
||||
|
||||
it('should infer TVariables from mutationFn parameter', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (vars: { id: string }) => Promise.resolve(vars.id),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' })
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer TOnMutateResult from onMutate return type', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onMutate: () => {
|
||||
return { token: 'abc' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ token: string }>()
|
||||
},
|
||||
onError: (_error, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<
|
||||
{ token: string } | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should allow explicit generic types', () => {
|
||||
const mutation = useMutation<string, Error, { id: number }>(() => ({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: number }>()
|
||||
return Promise.resolve('result')
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<Error | null>()
|
||||
})
|
||||
|
||||
it('should return correct UseMutationResult type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
UseMutationResult<number, DefaultError, void, unknown>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should type mutateAsync with correct return type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith('test')
|
||||
expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>()
|
||||
})
|
||||
|
||||
it('should default TVariables to void when mutationFn has no parameters', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (_variables: number | undefined) => Promise.resolve(1),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should infer custom TError type', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const mutation = useMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<CustomError | null>()
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types for onSettled callback', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
onSettled: (data, error, _variables, _onMutateResult) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should infer custom TError in onError callback', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
useMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<CustomError>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const mutation = useMutation(
|
||||
() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
expectTypeOf(context.client).toEqualTypeOf<QueryCoreClient>()
|
||||
return Promise.resolve('data')
|
||||
},
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should type context as the last argument for every per-call mutate option', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
mutation.mutate(undefined, {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type context.mutationKey as MutationKey', () => {
|
||||
useMutation(() => ({
|
||||
mutationKey: ['todos', 'add'] as const,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context.mutationKey).toEqualTypeOf<
|
||||
MutationKey | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
1845
packages/solid-query/src/__tests__/useMutation.test.tsx
Normal file
1845
packages/solid-query/src/__tests__/useMutation.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { useMutationState } from '../useMutationState'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationState,
|
||||
MutationStatus,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
it('should default to QueryState', () => {
|
||||
const result = useMutationState(() => ({
|
||||
filters: { status: 'pending' },
|
||||
}))
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<Array<MutationState>>()
|
||||
})
|
||||
it('should infer with select', () => {
|
||||
const result = useMutationState(() => ({
|
||||
filters: { status: 'pending' },
|
||||
select: (mutation) => mutation.state.status,
|
||||
}))
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<Array<MutationStatus>>()
|
||||
})
|
||||
it('should propagate generics to select callback when TResult is typed MutationState', () => {
|
||||
type MyData = { data: Array<string> }
|
||||
type MyError = { code: number; message: string }
|
||||
type MyVars = { id: number }
|
||||
|
||||
const result = useMutationState<MutationState<MyData, MyError, MyVars>>(
|
||||
() => ({
|
||||
filters: { mutationKey: ['key'] },
|
||||
select: (mutation) => {
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
Mutation<MyData, MyError, MyVars, unknown>
|
||||
>()
|
||||
return mutation.state
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<
|
||||
Array<MutationState<MyData, MyError, MyVars, unknown>>
|
||||
>()
|
||||
})
|
||||
})
|
||||
114
packages/solid-query/src/__tests__/useMutationState.test.tsx
Normal file
114
packages/solid-query/src/__tests__/useMutationState.test.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { createEffect } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useMutation, useMutationState } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return all mutation states when called without options', async () => {
|
||||
const mutationKey = queryKey()
|
||||
|
||||
function States() {
|
||||
const mutationStates = useMutationState()
|
||||
|
||||
return <div>count: {mutationStates().length}</div>
|
||||
}
|
||||
|
||||
function Mutate() {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationKey,
|
||||
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutation.mutate(1)}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<States />
|
||||
<Mutate />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('count: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('count: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
expect(rendered.getByText('count: 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return variables after calling mutate', async () => {
|
||||
const variables: Array<Array<unknown>> = []
|
||||
const mutationKey = queryKey()
|
||||
|
||||
function Variables() {
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey, status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
variables.push(states())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Mutate() {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationKey,
|
||||
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
data: {mutation.data ?? 'null'}
|
||||
<button onClick={() => mutation.mutate(1)}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Variables />
|
||||
<Mutate />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('data: null')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
expect(rendered.getByText('data: data1')).toBeInTheDocument()
|
||||
|
||||
expect(variables).toEqual([[], [1], []])
|
||||
})
|
||||
})
|
||||
894
packages/solid-query/src/__tests__/useQueries.test-d.tsx
Normal file
894
packages/solid-query/src/__tests__/useQueries.test-d.tsx
Normal file
@@ -0,0 +1,894 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { queryOptions, useQueries } from '..'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type * as QueryCore from '@tanstack/query-core'
|
||||
import type { OmitKeyof } from '@tanstack/query-core'
|
||||
import type {
|
||||
QueryFunction,
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
UseQueryResult,
|
||||
} from '..'
|
||||
import type { QueryOptions } from '../types'
|
||||
|
||||
describe('useQueries', () => {
|
||||
it('TData should have undefined in the union even when initialData is provided as an object', () => {
|
||||
const query1 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: false,
|
||||
},
|
||||
}
|
||||
|
||||
const query2 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 'Query Data',
|
||||
initialData: 'initial data',
|
||||
}
|
||||
|
||||
const query3 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 'Query Data',
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [query1, query2, query3],
|
||||
}))
|
||||
|
||||
const query1Data = queryResults[0].data
|
||||
const query2Data = queryResults[1].data
|
||||
const query3Data = queryResults[2].data
|
||||
|
||||
expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
expectTypeOf(query2Data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(query3Data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when passed through queryOptions', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: true,
|
||||
},
|
||||
})
|
||||
const queryResults = useQueries(() => ({ queries: [options] }))
|
||||
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types from explicit object type parameter', () => {
|
||||
const queryResults = useQueries<
|
||||
[
|
||||
{ queryFnData: number },
|
||||
{ queryFnData: string; error: Error },
|
||||
{ queryFnData: boolean; data: string },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
select: () => 'selected',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
|
||||
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types from explicit tuple type parameter', () => {
|
||||
const queryResults = useQueries<
|
||||
[[number], [string, Error], [boolean, Error, string]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
select: () => 'selected',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
|
||||
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => {
|
||||
const query1 = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data) => data > 1,
|
||||
})
|
||||
|
||||
const query2 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data: number) => data > 1,
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({ queries: [query1, query2] }))
|
||||
const query1Data = queryResults[0].data
|
||||
const query2Data = queryResults[1].data
|
||||
|
||||
expectTypeOf(query1Data).toEqualTypeOf<boolean | undefined>()
|
||||
expectTypeOf(query2Data).toEqualTypeOf<boolean | undefined>()
|
||||
})
|
||||
|
||||
describe('custom hook', () => {
|
||||
it('should allow custom hooks using QueryOptions', () => {
|
||||
type Data = string
|
||||
|
||||
const useCustomQueries = (
|
||||
options?: OmitKeyof<QueryOptions<Data>, 'queryKey' | 'queryFn'>,
|
||||
) => {
|
||||
return useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
...options,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
const queryResults = useCustomQueries()
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Data | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer custom TError from throwOnError', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
throwOnError: (_error: CustomError) => false,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].error).toEqualTypeOf<CustomError | null>()
|
||||
})
|
||||
|
||||
it('TData should have correct type when conditional skipToken is passed', () => {
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const firstResult = queryResults[0]
|
||||
|
||||
expectTypeOf(firstResult).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(firstResult.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should return correct data for dynamic queries with mixed result types', () => {
|
||||
const Queries1 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
}),
|
||||
}
|
||||
const Queries2 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
}),
|
||||
}
|
||||
|
||||
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
|
||||
const result = useQueries(() => ({
|
||||
queries: [...queries1List, { ...Queries2.get() }],
|
||||
}))
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
[...Array<UseQueryResult<number, Error>>, UseQueryResult<boolean, Error>]
|
||||
>()
|
||||
})
|
||||
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const queryResults = useQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer correct types for combine callback parameter', () => {
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('hello'),
|
||||
},
|
||||
],
|
||||
combine: (results) => {
|
||||
expectTypeOf(results[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(results[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
return results
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('type parameters', () => {
|
||||
it('should handle type parameter - tuple of tuples', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result1 = useQueries<
|
||||
[[number], [string], [Array<string>, boolean]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<UseQueryResult<number>>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<UseQueryResult<string>>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
UseQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (3rd element) takes precedence over TQueryFnData (1st element)
|
||||
const result2 = useQueries<
|
||||
[[string, unknown, string], [string, unknown, number]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
useQueries<[[string, unknown, string], [string, boolean, number]]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// field names should be enforced
|
||||
useQueries<[[string]]>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle type parameter - tuple of objects', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result1 = useQueries<
|
||||
[
|
||||
{ queryFnData: number },
|
||||
{ queryFnData: string },
|
||||
{ queryFnData: Array<string>; error: boolean },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
UseQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
|
||||
const result2 = useQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// can pass only TData (data prop) although TQueryFnData will be left unknown
|
||||
const result3 = useQueries<[{ data: string }, { data: number }]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as string
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as number
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
useQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number; error: boolean },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// field names should be enforced
|
||||
useQueries<[{ queryFnData: string }]>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle array literal without type parameter to infer result type', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
const key4 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
// Array.map preserves TQueryFnData
|
||||
const result1 = useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
})),
|
||||
}))
|
||||
expectTypeOf(result1).toEqualTypeOf<
|
||||
Array<UseQueryResult<number, Error>>
|
||||
>()
|
||||
if (result1[0]) {
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
}
|
||||
|
||||
// Array.map preserves TData
|
||||
const result2 = useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
expectTypeOf(result2).toEqualTypeOf<
|
||||
Array<UseQueryResult<string, Error>>
|
||||
>()
|
||||
|
||||
const result3 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
select: () => 123,
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result3[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<string | undefined>()
|
||||
// select takes precedence over queryFn
|
||||
expectTypeOf(result3[2].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// initialData/placeholderData are enforced
|
||||
useQueries(() => ({
|
||||
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,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// select params are "indirectly" enforced
|
||||
useQueries(() => ({
|
||||
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),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// callbacks are also indirectly enforced with Array.map
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// results inference works when all the handlers are defined
|
||||
const result4 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => 'string',
|
||||
select: (a: string) => parseInt(a),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result4[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
|
||||
// handles when queryFn returns a Promise
|
||||
const result5 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result5[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
|
||||
// Array as const does not throw error
|
||||
const result6 = useQueries(
|
||||
() =>
|
||||
({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 123,
|
||||
},
|
||||
],
|
||||
}) as const,
|
||||
)
|
||||
expectTypeOf(result6[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result6[1]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
|
||||
// field names should be enforced - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// field names should be enforced - Array.map() result
|
||||
useQueries(() => ({
|
||||
// @ts-expect-error (invalidField)
|
||||
queries: Array(10).map(() => ({
|
||||
someInvalidField: '',
|
||||
})),
|
||||
}))
|
||||
|
||||
// field names should be enforced - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - Array.map() result
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
})),
|
||||
}))
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle strongly typed queryFn factories and useQueries wrappers', () => {
|
||||
// QueryKey + queryFn factory
|
||||
type QueryKeyA = ['queryA']
|
||||
const getQueryKeyA = (): QueryKeyA => ['queryA']
|
||||
type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
|
||||
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<string, QueryKeyB>
|
||||
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<QueryOptions<TQueryFnData, TError, TData, TQueryKey>>) {
|
||||
return useQueries(() => ({
|
||||
queries: queries.map(
|
||||
// no need to type the mapped query
|
||||
(query) => {
|
||||
const { queryFn: fn, queryKey: key } = query
|
||||
expectTypeOf(fn).toEqualTypeOf<
|
||||
| typeof QueryCore.skipToken
|
||||
| QueryCore.QueryFunction<TQueryFnData, TQueryKey, never>
|
||||
| undefined
|
||||
>()
|
||||
return {
|
||||
queryKey: key,
|
||||
queryFn: fn
|
||||
? (ctx: QueryFunctionContext<TQueryKey>) => {
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
expectTypeOf<TQueryKey>(ctx.queryKey)
|
||||
return (
|
||||
fn as QueryFunction<TQueryFnData, TQueryKey>
|
||||
).call({}, ctx)
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
|
||||
const withSelector = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
select: getSelectorB(),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(withSelector[0]).toEqualTypeOf<
|
||||
UseQueryResult<[number, string], Error>
|
||||
>()
|
||||
expectTypeOf(withSelector[1]).toEqualTypeOf<
|
||||
UseQueryResult<[string, number], Error>
|
||||
>()
|
||||
|
||||
const withWrappedQueries = useWrappedQueries(
|
||||
Array(10).map(() => ({
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
})),
|
||||
)
|
||||
|
||||
expectTypeOf(withWrappedQueries).toEqualTypeOf<
|
||||
Array<UseQueryResult<number, Error>>
|
||||
>()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
216
packages/solid-query/src/__tests__/useQueries.test.tsx
Normal file
216
packages/solid-query/src/__tests__/useQueries.test.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import * as QueryCore from '@tanstack/query-core'
|
||||
import { createRenderEffect, createSignal } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import {
|
||||
IsRestoringProvider,
|
||||
QueriesObserver,
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
useQueries,
|
||||
} from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { UseQueryResult } from '..'
|
||||
|
||||
describe('useQueries', () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the correct states', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const results: Array<Array<UseQueryResult>> = []
|
||||
|
||||
function Page() {
|
||||
const result = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 1),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(100).then(() => 2),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
results.push([{ ...result[0] }, { ...result[1] }])
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
data1: {String(result[0].data ?? 'null')}, data2:{' '}
|
||||
{String(result[1].data ?? 'null')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data1: 1, data2: 2')).toBeInTheDocument()
|
||||
|
||||
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 }])
|
||||
})
|
||||
|
||||
// eslint-disable-next-line vitest/expect-expect
|
||||
it('should not change state if unmounted', async () => {
|
||||
const key1 = queryKey()
|
||||
|
||||
// We have to mock the QueriesObserver to not unsubscribe
|
||||
// the listener when the component is unmounted
|
||||
class QueriesObserverMock extends QueriesObserver {
|
||||
subscribe(listener: any) {
|
||||
super.subscribe(listener)
|
||||
return () => void 0
|
||||
}
|
||||
}
|
||||
|
||||
const QueriesObserverSpy = vi
|
||||
.spyOn(QueryCore, 'QueriesObserver')
|
||||
.mockImplementation(function (
|
||||
client: InstanceType<typeof QueryCore.QueryClient>,
|
||||
queries: Array<QueryCore.QueryObserverOptions>,
|
||||
) {
|
||||
return new QueriesObserverMock(client, queries)
|
||||
})
|
||||
|
||||
function Queries() {
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 1),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>queries</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [mounted, setMounted] = createSignal(true)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMounted(false)}>unmount</button>
|
||||
{mounted() && <Queries />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
fireEvent.click(rendered.getByText('unmount'))
|
||||
|
||||
// Should not display the console error
|
||||
// "Warning: Can't perform a React state update on an unmounted component"
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
QueriesObserverSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const key = queryKey()
|
||||
const queryFn = () => sleep(10).then(() => 'custom client')
|
||||
|
||||
function Page() {
|
||||
const queries = useQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
return <div>data: {queries[0].data}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: custom client')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1'))
|
||||
const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2'))
|
||||
|
||||
function Page() {
|
||||
const results = useQueries(() => ({
|
||||
queries: [
|
||||
{ queryKey: key1, queryFn: queryFn1 },
|
||||
{ queryKey: key2, queryFn: queryFn2 },
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="status1">{results[0].status}</div>
|
||||
<div data-testid="status2">{results[1].status}</div>
|
||||
<div data-testid="fetchStatus1">{results[0].fetchStatus}</div>
|
||||
<div data-testid="fetchStatus2">{results[1].fetchStatus}</div>
|
||||
<div data-testid="data1">{results[0].data ?? 'undefined'}</div>
|
||||
<div data-testid="data2">{results[1].data ?? 'undefined'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<IsRestoringProvider value={() => true}>
|
||||
<Page />
|
||||
</IsRestoringProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
310
packages/solid-query/src/__tests__/useQuery.test-d.tsx
Normal file
310
packages/solid-query/src/__tests__/useQuery.test-d.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { queryOptions, useQuery } from '../index'
|
||||
import type { OmitKeyof, QueryFunction, UseQueryOptions } from '..'
|
||||
|
||||
describe('useQuery', () => {
|
||||
const key = queryKey()
|
||||
|
||||
// unspecified query function should default to unknown
|
||||
const noQueryFn = useQuery(() => ({ queryKey: key }))
|
||||
expectTypeOf(noQueryFn.data).toEqualTypeOf<unknown>()
|
||||
expectTypeOf(noQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should infer the result type from the query function
|
||||
const fromQueryFn = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(fromQueryFn.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(fromQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should be possible to specify the result type
|
||||
const withResult = useQuery<string>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(withResult.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(withResult.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should be possible to specify the error type
|
||||
const withError = useQuery<string, Error>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(withError.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(withError.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should provide the result type in the configuration
|
||||
useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn: () => true,
|
||||
}))
|
||||
|
||||
// it should be possible to specify a union type as result type
|
||||
const unionTypeSync = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)),
|
||||
}))
|
||||
expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b' | undefined>()
|
||||
const unionTypeAsync = useQuery<'a' | 'b'>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'),
|
||||
}))
|
||||
expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b' | undefined>()
|
||||
|
||||
// should error when the query function result does not match with the specified type
|
||||
// @ts-expect-error
|
||||
useQuery<number>(() => ({ queryKey: key, queryFn: () => 'test' }))
|
||||
|
||||
// it should infer the result type from a generic query function
|
||||
function queryFn<T = string>(): Promise<T> {
|
||||
return Promise.resolve({} as T)
|
||||
}
|
||||
|
||||
const fromGenericQueryFn = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => queryFn(),
|
||||
}))
|
||||
expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
type MyData = number
|
||||
type MyQueryKey = readonly ['my-data', number]
|
||||
|
||||
const getMyDataArrayKey: QueryFunction<MyData, MyQueryKey> = ({
|
||||
queryKey: [, n],
|
||||
}) => {
|
||||
return n + 42
|
||||
}
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: ['my-data', 100] as const,
|
||||
queryFn: getMyDataArrayKey,
|
||||
}))
|
||||
|
||||
const getMyDataStringKey: QueryFunction<MyData, ['1']> = (context) => {
|
||||
expectTypeOf(context.queryKey).toEqualTypeOf<['1']>()
|
||||
return Number(context.queryKey[0]) + 42
|
||||
}
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: ['1'] as ['1'],
|
||||
queryFn: getMyDataStringKey,
|
||||
}))
|
||||
|
||||
// it should handle query-functions that return Promise<any>
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
}))
|
||||
|
||||
// handles wrapped queries with custom fetcher passed as inline queryFn
|
||||
const useWrappedQuery = <
|
||||
TQueryKey extends [string, Record<string, unknown>?],
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData = TQueryFnData,
|
||||
>(
|
||||
qk: TQueryKey,
|
||||
fetcher: (
|
||||
obj: TQueryKey[1],
|
||||
token: string,
|
||||
// return type must be wrapped with TQueryFnReturn
|
||||
) => Promise<TQueryFnData>,
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryKey' | 'queryFn' | 'initialData',
|
||||
'safely'
|
||||
>,
|
||||
) =>
|
||||
useQuery(() => ({
|
||||
queryKey: qk,
|
||||
queryFn: () => fetcher(qk[1], 'token'),
|
||||
...options,
|
||||
}))
|
||||
const test = useWrappedQuery([''], () => Promise.resolve('1'))
|
||||
expectTypeOf(test.data).toEqualTypeOf<string | undefined>()
|
||||
|
||||
// handles wrapped queries with custom fetcher passed directly to useQuery
|
||||
const useWrappedFuncStyleQuery = <
|
||||
TQueryKey extends [string, Record<string, unknown>?],
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData = TQueryFnData,
|
||||
>(
|
||||
qk: TQueryKey,
|
||||
fetcher: () => Promise<TQueryFnData>,
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryKey' | 'queryFn' | 'initialData',
|
||||
'safely'
|
||||
>,
|
||||
) => useQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options }))
|
||||
const testFuncStyle = useWrappedFuncStyleQuery([''], () =>
|
||||
Promise.resolve(true),
|
||||
)
|
||||
expectTypeOf(testFuncStyle.data).toEqualTypeOf<boolean | undefined>()
|
||||
|
||||
it('should return the correct states for a successful query', () => {
|
||||
const state = useQuery<string, Error>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('test'),
|
||||
}))
|
||||
|
||||
if (state.isPending) {
|
||||
expectTypeOf(state.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<null>()
|
||||
} else if (state.isLoadingError) {
|
||||
expectTypeOf(state.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<Error>()
|
||||
} else {
|
||||
expectTypeOf(state.data).toEqualTypeOf<string>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<Error | null>()
|
||||
}
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
describe('Config object overload', () => {
|
||||
it('TData should always be defined when initialData is provided as an object', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should be defined when passed through queryOptions', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
})
|
||||
const { data } = useQuery(() => options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key overload', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key and func', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('generic indexed access TData', () => {
|
||||
// https://github.com/TanStack/query/issues/9937
|
||||
it('should be assignable back to its source indexed type when passed to a generic function parameter', () => {
|
||||
enum DataType {
|
||||
Account = 'account',
|
||||
Product = 'product',
|
||||
}
|
||||
|
||||
interface Account {
|
||||
name: string
|
||||
}
|
||||
interface Product {
|
||||
code: string
|
||||
}
|
||||
|
||||
type DataTypeToEntity = {
|
||||
[DataType.Account]: Account
|
||||
[DataType.Product]: Product
|
||||
}
|
||||
|
||||
const getData = <TDataType extends DataType>(
|
||||
_dataType: TDataType,
|
||||
): Promise<DataTypeToEntity[TDataType]> =>
|
||||
Promise.resolve({} as DataTypeToEntity[TDataType])
|
||||
|
||||
const getLabel = <TDataType extends DataType>(
|
||||
_dataType: TDataType,
|
||||
_data: DataTypeToEntity[TDataType],
|
||||
) => 'test'
|
||||
|
||||
function Test<TDataType extends DataType>(props: {
|
||||
dataType: TDataType
|
||||
}) {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: ['test'],
|
||||
queryFn: () => getData(props.dataType),
|
||||
}))
|
||||
|
||||
// Regression guard: this call must compile. With the previous
|
||||
// hand-rolled NoInfer, `data` failed to flow back into the generic
|
||||
// indexed-access parameter `DataTypeToEntity[TDataType]`.
|
||||
return data ? getLabel(props.dataType, data) : null
|
||||
}
|
||||
|
||||
expectTypeOf(Test).toBeFunction()
|
||||
})
|
||||
})
|
||||
})
|
||||
5816
packages/solid-query/src/__tests__/useQuery.test.tsx
Normal file
5816
packages/solid-query/src/__tests__/useQuery.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
49
packages/solid-query/src/__tests__/utils.tsx
Normal file
49
packages/solid-query/src/__tests__/utils.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { vi } from 'vitest'
|
||||
import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
||||
import { onlineManager } from '@tanstack/query-core'
|
||||
import { render } from '@solidjs/testing-library'
|
||||
import { QueryClientProvider } from '..'
|
||||
import type { JSX, ParentProps } from 'solid-js'
|
||||
import type { MockInstance } from 'vitest'
|
||||
import type { QueryClient } from '..'
|
||||
|
||||
export function renderWithClient(
|
||||
client: QueryClient,
|
||||
ui: () => JSX.Element,
|
||||
): ReturnType<typeof render> {
|
||||
return render(() => (
|
||||
<QueryClientProvider client={client}>{ui()}</QueryClientProvider>
|
||||
))
|
||||
}
|
||||
|
||||
export function Blink(
|
||||
props: {
|
||||
duration: number
|
||||
} & ParentProps,
|
||||
) {
|
||||
const [shouldShow, setShouldShow] = createSignal<boolean>(true)
|
||||
|
||||
createEffect(() => {
|
||||
setShouldShow(true)
|
||||
const timeout = setActTimeout(() => setShouldShow(false), props.duration)
|
||||
onCleanup(() => clearTimeout(timeout))
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={shouldShow()} fallback={<>off</>}>
|
||||
<>{props.children}</>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function mockOnlineManagerIsOnline(
|
||||
value: boolean,
|
||||
): MockInstance<() => boolean> {
|
||||
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
|
||||
}
|
||||
|
||||
export function setActTimeout(fn: () => void, ms?: number) {
|
||||
return setTimeout(() => {
|
||||
fn()
|
||||
}, ms)
|
||||
}
|
||||
Reference in New Issue
Block a user