snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,736 @@
import { describe, expect, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { sleep } from '@tanstack/query-test-utils'
import type { ReactiveController, ReactiveControllerHost } from 'lit'
import { QueryClientProvider } from '../QueryClientProvider.js'
import { createMutationController } from '../createMutationController.js'
import { createQueryController } from '../createQueryController.js'
import { useIsFetching } from '../useIsFetching.js'
import { useIsMutating } from '../useIsMutating.js'
import { useMutationState } from '../useMutationState.js'
import {
TestControllerHost,
TestElementHost,
waitFor,
waitForMissingQueryClient,
} from './testHost.js'
const providerTagName = 'test-query-client-provider-counters'
if (!customElements.get(providerTagName)) {
customElements.define(providerTagName, QueryClientProvider)
}
let explicitCountersClient: QueryClient | undefined
class ContextCountersHostElement extends TestElementHost {
readonly queryKey = ['context-counters', 'query'] as const
readonly mutationKey = ['context-counters', 'mutation'] as const
private resolveQuery: (() => void) | undefined
private resolveMutation: (() => void) | undefined
readonly query = createQueryController(
this,
{
queryKey: this.queryKey,
queryFn: () =>
new Promise<string>((resolve) => {
this.resolveQuery = () => resolve('query-ok')
}),
retry: false,
},
explicitCountersClient,
)
readonly mutation = createMutationController(
this,
{
mutationKey: this.mutationKey,
mutationFn: () =>
new Promise<string>((resolve) => {
this.resolveMutation = () => resolve('mutation-ok')
}),
},
explicitCountersClient,
)
readonly isFetching = useIsFetching(
this,
{ queryKey: this.queryKey },
explicitCountersClient,
)
readonly isMutating = useIsMutating(
this,
{ mutationKey: this.mutationKey },
explicitCountersClient,
)
readonly mutationStatuses = useMutationState<string>(
this,
{
filters: { mutationKey: this.mutationKey },
select: (mutation) => mutation.state.status,
},
explicitCountersClient,
)
resolvePendingQuery(): void {
this.resolveQuery?.()
}
resolvePendingMutation(): void {
this.resolveMutation?.()
}
}
const contextCountersTagName = 'test-context-counters-host'
if (!customElements.get(contextCountersTagName)) {
customElements.define(contextCountersTagName, ContextCountersHostElement)
}
describe('useIsFetching/useIsMutating/useMutationState', () => {
it('does not request another update when stable mutation state selectors refresh during host update', async () => {
const client = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
})
const host = new TestControllerHost()
const mutationStates = useMutationState<string>(
host,
{
select: (mutation) => mutation.state.status,
},
client,
)
try {
host.connect()
await Promise.resolve()
expect(mutationStates()).toEqual([])
host.updatesRequested = 0
for (let i = 0; i < 5; i += 1) {
host.update()
await Promise.resolve()
}
expect(host.updatesRequested).toBe(0)
expect(mutationStates()).toEqual([])
} finally {
mutationStates.destroy()
}
})
it('does not request another update when mutation cache emits with unchanged selected state', async () => {
const client = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
})
const mutationKey = ['mutation-state', 'stable-cache-events'] as const
const mutation = client.getMutationCache().build(client, {
mutationKey,
})
const host = new TestControllerHost()
const mutationStates = useMutationState<string>(
host,
{
filters: { mutationKey },
select: (mutation) => mutation.state.status,
},
client,
)
try {
host.connect()
await Promise.resolve()
expect(mutationStates()).toEqual(['idle'])
host.updatesRequested = 0
for (let i = 0; i < 5; i += 1) {
client.getMutationCache().notify({
type: 'updated',
mutation,
action: { type: 'pause' } as never,
})
await Promise.resolve()
}
expect(host.updatesRequested).toBe(0)
expect(mutationStates()).toEqual(['idle'])
} finally {
mutationStates.destroy()
}
})
it('LC-COUNTERS-01: pre-connect placeholders stay zero/empty until a provider binds', async () => {
const consumer = document.createElement(
contextCountersTagName,
) as ContextCountersHostElement
expect(consumer.query().status).toBe('pending')
expect(consumer.isFetching()).toBe(0)
expect(consumer.isMutating()).toBe(0)
expect(consumer.mutationStatuses()).toEqual([])
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
})
const provider = document.createElement(
providerTagName,
) as QueryClientProvider
provider.client = client
provider.append(consumer)
document.body.append(provider)
await provider.updateComplete
await consumer.updateComplete
await waitFor(() => consumer.isFetching() === 1)
consumer.resolvePendingQuery()
await waitFor(() => consumer.query().isSuccess)
await waitFor(() => consumer.isFetching() === 0)
consumer.mutation.mutate()
await waitFor(() => consumer.isMutating() === 1)
consumer.resolvePendingMutation()
await waitFor(() => consumer.isMutating() === 0)
await waitFor(() => consumer.mutationStatuses().includes('success'))
consumer.query.destroy()
consumer.mutation.destroy()
consumer.isFetching.destroy()
consumer.isMutating.destroy()
consumer.mutationStatuses.destroy()
provider.remove()
await Promise.resolve()
})
it('LC-COUNTERS-02: explicit client takes precedence over provider context', async () => {
const explicitClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
})
const providerClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
})
explicitCountersClient = explicitClient
const provider = document.createElement(
providerTagName,
) as QueryClientProvider
provider.client = providerClient
const consumer = document.createElement(
contextCountersTagName,
) as ContextCountersHostElement
provider.append(consumer)
document.body.append(provider)
await provider.updateComplete
await consumer.updateComplete
await waitFor(() => consumer.isFetching() === 1)
consumer.resolvePendingQuery()
await waitFor(() => consumer.query().isSuccess)
consumer.mutation.mutate()
await waitFor(() => consumer.isMutating() === 1)
consumer.resolvePendingMutation()
await waitFor(() => consumer.isMutating() === 0)
expect(
explicitClient.getQueryCache().find({ queryKey: consumer.queryKey })
?.state.data,
).toBe('query-ok')
expect(
providerClient.getQueryCache().find({ queryKey: consumer.queryKey }),
).toBeUndefined()
expect(
explicitClient
.getMutationCache()
.findAll({ mutationKey: consumer.mutationKey }).length,
).toBeGreaterThan(0)
expect(
providerClient
.getMutationCache()
.findAll({ mutationKey: consumer.mutationKey }).length,
).toBe(0)
consumer.query.destroy()
consumer.mutation.destroy()
consumer.isFetching.destroy()
consumer.isMutating.destroy()
consumer.mutationStatuses.destroy()
provider.remove()
explicitCountersClient = undefined
await Promise.resolve()
})
it('tracks fetch/mutate counters and mutation state', async () => {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const host = new TestControllerHost()
const query = createQueryController(
host,
{
queryKey: ['counter-test'],
queryFn: async () => {
await sleep(40)
return 'done'
},
},
client,
)
const mutation = createMutationController(
host,
{
mutationFn: async (value: number) => {
await sleep(40)
return value + 10
},
},
client,
)
const isFetching = useIsFetching(host, {}, client)
const isMutating = useIsMutating(host, {}, client)
const mutationStatuses = useMutationState<string>(
host,
{
select: (item) => item.state.status,
},
client,
)
host.connect()
host.update()
await waitFor(() => isFetching() === 1)
await waitFor(() => query().isSuccess)
await waitFor(() => isFetching() === 0)
mutation.mutate(1)
await waitFor(() => isMutating() === 1)
await waitFor(() => isMutating() === 0)
await waitFor(() => mutationStatuses().includes('success'))
})
it('S1: useIsFetching tracks filters and filter reactivity', async () => {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const host = new TestControllerHost()
let resolveA: (() => void) | undefined
let resolveB: (() => void) | undefined
let activeFilter: { queryKey?: readonly string[] } = {
queryKey: ['fetch-a'],
}
createQueryController(
host,
{
queryKey: ['fetch-a'],
queryFn: () =>
new Promise<string>((resolve) => {
resolveA = () => resolve('a')
}),
},
client,
)
createQueryController(
host,
{
queryKey: ['fetch-b'],
queryFn: () =>
new Promise<string>((resolve) => {
resolveB = () => resolve('b')
}),
},
client,
)
const isFetchingAll = useIsFetching(host, {}, client)
const isFetchingFiltered = useIsFetching(host, () => activeFilter, client)
host.connect()
host.update()
await waitFor(() => isFetchingAll() === 2)
await waitFor(() => isFetchingFiltered() === 1)
activeFilter = { queryKey: ['fetch-b'] }
host.update()
await waitFor(() => isFetchingFiltered() === 1)
resolveA?.()
await waitFor(() => isFetchingAll() === 1)
await waitFor(() => isFetchingFiltered() === 1)
resolveB?.()
await waitFor(() => isFetchingAll() === 0)
await waitFor(() => isFetchingFiltered() === 0)
})
it('S2: useIsMutating tracks mutation filters and reactivity', async () => {
const client = new QueryClient()
const host = new TestControllerHost()
let resolveA: (() => void) | undefined
let resolveB: (() => void) | undefined
let activeFilter: { mutationKey?: readonly string[] } = {
mutationKey: ['mut-a'],
}
const mutationA = createMutationController(
host,
{
mutationKey: ['mut-a'],
mutationFn: () =>
new Promise<number>((resolve) => {
resolveA = () => resolve(1)
}),
},
client,
)
const mutationB = createMutationController(
host,
{
mutationKey: ['mut-b'],
mutationFn: () =>
new Promise<number>((resolve) => {
resolveB = () => resolve(2)
}),
},
client,
)
const isMutatingAll = useIsMutating(host, {}, client)
const isMutatingFiltered = useIsMutating(host, () => activeFilter, client)
host.connect()
host.update()
mutationA.mutate()
mutationB.mutate()
await waitFor(() => isMutatingAll() === 2)
await waitFor(() => isMutatingFiltered() === 1)
activeFilter = { mutationKey: ['mut-b'] }
host.update()
await waitFor(() => isMutatingFiltered() === 1)
resolveA?.()
await waitFor(() => isMutatingAll() === 1)
await waitFor(() => isMutatingFiltered() === 1)
resolveB?.()
await waitFor(() => isMutatingAll() === 0)
await waitFor(() => isMutatingFiltered() === 0)
})
it('S3: useMutationState selects and filters by mutation key/status', async () => {
const client = new QueryClient()
const host = new TestControllerHost()
let activeFilter: { mutationKey?: readonly string[] } = {
mutationKey: ['state-a'],
}
const mutationA = createMutationController(
host,
{
mutationKey: ['state-a'],
mutationFn: async () => 'ok',
},
client,
)
const mutationB = createMutationController(
host,
{
mutationKey: ['state-b'],
mutationFn: async () => {
throw new Error('state-b-failure')
},
},
client,
)
const mutationStatuses = useMutationState<string>(
host,
{
filters: () => activeFilter,
select: (item) => item.state.status,
},
client,
)
host.connect()
host.update()
await expect(mutationA.mutateAsync(undefined)).resolves.toBe('ok')
await expect(mutationB.mutateAsync(undefined)).rejects.toThrow(
'state-b-failure',
)
await waitFor(
() =>
mutationStatuses().length === 1 && mutationStatuses()[0] === 'success',
)
activeFilter = { mutationKey: ['state-b'] }
host.update()
await waitFor(
() =>
mutationStatuses().length === 1 && mutationStatuses()[0] === 'error',
)
})
it('S4: useMutationState refreshes when the select closure changes on host update', async () => {
const client = new QueryClient()
const host = new TestControllerHost()
let label = 'before'
const mutation = createMutationController(
host,
{
mutationKey: ['state-select-reactivity'],
mutationFn: async () => 'ok',
},
client,
)
const mutationLabels = useMutationState<string>(
host,
{
filters: {
mutationKey: ['state-select-reactivity'],
},
select: () => label,
},
client,
)
host.connect()
host.update()
await expect(mutation.mutateAsync(undefined)).resolves.toBe('ok')
await waitFor(
() => mutationLabels().length === 1 && mutationLabels()[0] === 'before',
)
label = 'after'
host.update()
await waitFor(
() => mutationLabels().length === 1 && mutationLabels()[0] === 'after',
)
mutation.destroy()
mutationLabels.destroy()
})
it('LC-COUNTERS-03: read-only helpers fail after handshake and recover under a provider', async () => {
const consumer = document.createElement(
contextCountersTagName,
) as ContextCountersHostElement
expect(consumer.query().status).toBe('pending')
expect(consumer.isFetching()).toBe(0)
expect(consumer.isMutating()).toBe(0)
expect(consumer.mutationStatuses()).toEqual([])
document.body.append(consumer)
await waitForMissingQueryClient(() => consumer.query())
expect(() => consumer.isFetching()).toThrow(/No QueryClient available/)
expect(() => consumer.isMutating()).toThrow(/No QueryClient available/)
expect(() => consumer.mutationStatuses()).toThrow(
/No QueryClient available/,
)
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
})
const provider = document.createElement(
providerTagName,
) as QueryClientProvider
provider.client = client
provider.append(consumer)
document.body.append(provider)
await provider.updateComplete
await consumer.updateComplete
await waitFor(() => consumer.isFetching() === 1)
consumer.resolvePendingQuery()
await waitFor(() => consumer.query().isSuccess)
await waitFor(() => consumer.isFetching() === 0)
consumer.query.destroy()
consumer.mutation.destroy()
consumer.isFetching.destroy()
consumer.isMutating.destroy()
consumer.mutationStatuses.destroy()
provider.remove()
await Promise.resolve()
})
it('ALREADYCONN-COUNTERS-01: read-only helpers on already-connected host with explicit client do not throw', async () => {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
})
const producerHost = new TestControllerHost()
let resolveQuery: (() => void) | undefined
let resolveMutation: (() => void) | undefined
createQueryController(
producerHost,
{
queryKey: ['already-connected-counters-query'],
queryFn: () =>
new Promise<string>((resolve) => {
resolveQuery = () => resolve('query-ok')
}),
retry: false,
},
client,
)
const producerMutation = createMutationController(
producerHost,
{
mutationKey: ['already-connected-counters-mutation'],
mutationFn: () =>
new Promise<string>((resolve) => {
resolveMutation = () => resolve('mutation-ok')
}),
},
client,
)
producerHost.connect()
producerHost.update()
await waitFor(() => client.isFetching() === 1)
producerMutation.mutate()
await waitFor(() => client.isMutating() === 1)
class AlreadyConnectedHost implements ReactiveControllerHost {
private readonly controllers = new Set<ReactiveController>()
private isConnected = true
updatesRequested = 0
readonly updateComplete: Promise<boolean> = Promise.resolve(true)
addController(controller: ReactiveController): void {
this.controllers.add(controller)
if (this.isConnected) {
controller.hostConnected?.()
}
}
removeController(controller: ReactiveController): void {
this.controllers.delete(controller)
}
requestUpdate(): void {
this.updatesRequested += 1
}
}
const host = new AlreadyConnectedHost()
const isFetching = useIsFetching(host, {}, client)
const isMutating = useIsMutating(host, {}, client)
const mutationStatuses = useMutationState<string>(
host,
{
filters: { mutationKey: ['already-connected-counters-mutation'] },
select: (mutation) => mutation.state.status,
},
client,
)
await Promise.resolve()
await Promise.resolve()
await waitFor(() => isFetching() === 1)
await waitFor(() => isMutating() === 1)
await waitFor(() => mutationStatuses().includes('pending'))
resolveQuery?.()
resolveMutation?.()
await waitFor(() => isFetching() === 0)
await waitFor(() => isMutating() === 0)
await waitFor(() => mutationStatuses().includes('success'))
isFetching.destroy()
isMutating.destroy()
mutationStatuses.destroy()
producerMutation.destroy()
})
})