gs-port / node_modules /@reduxjs /toolkit /src /query /tests /buildThunks.test.tsx
Scribbler310's picture
feat: enhance dashboard
c2b7eb3 verified
import { configureStore } from '@reduxjs/toolkit'
import { createApi } from '@reduxjs/toolkit/query/react'
import { renderHook, waitFor } from '@testing-library/react'
import {
actionsReducer,
setupApiStore,
withProvider,
} from '../../tests/utils/helpers'
import type { BaseQueryApi } from '../baseQueryTypes'
describe('baseline thunk behavior', () => {
test('handles a non-async baseQuery without error', async () => {
const baseQuery = (args?: any) => ({ data: args })
const api = createApi({
baseQuery,
endpoints: (build) => ({
getUser: build.query<unknown, number>({
query(id) {
return { url: `user/${id}` }
},
}),
}),
})
const { getUser } = api.endpoints
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
const promise = store.dispatch(getUser.initiate(1))
const { data } = await promise
expect(data).toEqual({
url: 'user/1',
})
const storeResult = getUser.select(1)(store.getState())
expect(storeResult).toEqual({
data: {
url: 'user/1',
},
endpointName: 'getUser',
isError: false,
isLoading: false,
isSuccess: true,
isUninitialized: false,
originalArgs: 1,
requestId: expect.any(String),
status: 'fulfilled',
startedTimeStamp: expect.any(Number),
fulfilledTimeStamp: expect.any(Number),
})
})
test('passes the extraArgument property to the baseQueryApi', async () => {
const baseQuery = (_args: any, api: BaseQueryApi) => ({ data: api.extra })
const api = createApi({
baseQuery,
endpoints: (build) => ({
getUser: build.query<unknown, void>({
query: () => '',
}),
}),
})
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) =>
gDM({ thunk: { extraArgument: 'cakes' } }).concat(api.middleware),
})
const { getUser } = api.endpoints
const { data } = await store.dispatch(getUser.initiate())
expect(data).toBe('cakes')
})
test('only triggers transformResponse when a query method is actually used', async () => {
const baseQuery = (args?: any) => ({ data: args })
const transformResponse = vi.fn((response: any) => response)
const api = createApi({
baseQuery,
endpoints: (build) => ({
hasQuery: build.query<string, string>({
query: (arg) => 'test',
transformResponse,
}),
hasQueryFn: build.query<string, void>(
// @ts-expect-error
{
queryFn: () => ({ data: 'test' }),
transformResponse,
},
),
}),
})
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) =>
gDM({ thunk: { extraArgument: 'cakes' } }).concat(api.middleware),
})
await store.dispatch(api.util.upsertQueryData('hasQuery', 'a', 'test'))
expect(transformResponse).not.toHaveBeenCalled()
transformResponse.mockReset()
await store.dispatch(api.endpoints.hasQuery.initiate('b'))
expect(transformResponse).toHaveBeenCalledTimes(1)
transformResponse.mockReset()
await store.dispatch(api.endpoints.hasQueryFn.initiate())
expect(transformResponse).not.toHaveBeenCalled()
})
})
describe('re-triggering behavior on arg change', () => {
const api = createApi({
baseQuery: () => ({ data: null }),
endpoints: (build) => ({
getUser: build.query<any, any>({
query: (obj) => obj,
}),
}),
})
const { getUser } = api.endpoints
const store = configureStore({
reducer: { [api.reducerPath]: api.reducer },
middleware: (gDM) => gDM().concat(api.middleware),
})
const spy = vi.spyOn(getUser, 'initiate')
beforeEach(() => void spy.mockClear())
test('re-trigger on literal value change', async () => {
const { result, rerender } = renderHook(
(props) => getUser.useQuery(props),
{
wrapper: withProvider(store),
initialProps: 5,
},
)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledOnce()
for (let x = 1; x < 3; x++) {
rerender(6)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledTimes(2)
}
for (let x = 1; x < 3; x++) {
rerender(7)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledTimes(3)
}
})
test('only re-trigger on shallow-equal arg change', async () => {
const { result, rerender } = renderHook(
(props) => getUser.useQuery(props),
{
wrapper: withProvider(store),
initialProps: { name: 'Bob', likes: 'iceCream' },
},
)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledOnce()
for (let x = 1; x < 3; x++) {
rerender({ name: 'Bob', likes: 'waffles' })
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledTimes(2)
}
for (let x = 1; x < 3; x++) {
rerender({ name: 'Alice', likes: 'waffles' })
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledTimes(3)
}
})
test('re-triggers every time on deeper value changes', async () => {
const name = 'Tim'
const { result, rerender } = renderHook(
(props) => getUser.useQuery(props),
{
wrapper: withProvider(store),
initialProps: { person: { name } },
},
)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledOnce()
for (let x = 1; x < 3; x++) {
rerender({ person: { name: name + x } })
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledTimes(x + 1)
}
})
test('do not re-trigger if the order of keys change while maintaining the same values', async () => {
const { result, rerender } = renderHook(
(props) => getUser.useQuery(props),
{
wrapper: withProvider(store),
initialProps: { name: 'Tim', likes: 'Bananas' },
},
)
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledOnce()
for (let x = 1; x < 3; x++) {
rerender({ likes: 'Bananas', name: 'Tim' })
await waitFor(() => {
expect(result.current.status).not.toBe('pending')
})
expect(spy).toHaveBeenCalledOnce()
}
})
})
describe('prefetch', () => {
const baseQuery = () => ({ data: { name: 'Test User' } })
const api = createApi({
baseQuery,
tagTypes: ['User'],
endpoints: (build) => ({
getUser: build.query<any, number>({
query: (id) => ({ url: `user/${id}` }),
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
updateUser: build.mutation<any, { id: number; name: string }>({
query: ({ id, name }) => ({
url: `user/${id}`,
method: 'PUT',
body: { name },
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
}),
keepUnusedDataFor: 0.1, // 100ms for faster test cleanup
})
let storeRef = setupApiStore(
api,
{ ...actionsReducer },
{
withoutListeners: true,
},
)
let getSubscriptions: () => Map<string, any>
let getSubscriptionCount: (queryCacheKey: string) => number
beforeEach(() => {
storeRef = setupApiStore(
api,
{ ...actionsReducer },
{
withoutListeners: true,
},
)
// Get subscription helpers
const subscriptionSelectors = storeRef.store.dispatch(
api.internalActions.internal_getRTKQSubscriptions(),
) as any
getSubscriptions = subscriptionSelectors.getSubscriptions
getSubscriptionCount = subscriptionSelectors.getSubscriptionCount
})
describe('subscription behavior', () => {
it('prefetch should NOT create a subscription', async () => {
const queryCacheKey = 'getUser(1)'
// Initially no subscriptions
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
// Dispatch prefetch
storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
})
it('prefetch allows cache cleanup after keepUnusedDataFor', async () => {
const queryCacheKey = 'getUser(1)'
// Prefetch the data
storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
// Verify data is in cache
let state = api.endpoints.getUser.select(1)(storeRef.store.getState())
expect(state.data).toEqual({ name: 'Test User' })
// Wait longer than keepUnusedDataFor
await new Promise((resolve) => setTimeout(resolve, 150))
state = api.endpoints.getUser.select(1)(storeRef.store.getState())
expect(state.status).toBe('uninitialized')
expect(state.data).toBeUndefined()
})
it('prefetch does NOT trigger refetch on tag invalidation', async () => {
// Prefetch user 1
storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
// Verify data is in cache
let state = api.endpoints.getUser.select(1)(storeRef.store.getState())
expect(state.data).toEqual({ name: 'Test User' })
// Invalidate the tag by updating the user
await storeRef.store.dispatch(
api.endpoints.updateUser.initiate({ id: 1, name: 'Updated' }),
)
// Since there's no subscription, the cache entry gets removed on invalidation
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
// Cache entry should be cleared (no subscription to keep it alive)
state = api.endpoints.getUser.select(1)(storeRef.store.getState())
expect(state.status).toBe('uninitialized')
expect(state.data).toBeUndefined()
})
it('multiple prefetches do not accumulate subscriptions', async () => {
const queryCacheKey = 'getUser(1)'
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
// First prefetch
storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
// Second prefetch (force refetch)
storeRef.store.dispatch(api.util.prefetch('getUser', 1, { force: true }))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
// Still no subscriptions
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
// Third prefetch
storeRef.store.dispatch(api.util.prefetch('getUser', 1, { force: true }))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
})
it('prefetch followed by regular query should work correctly', async () => {
const queryCacheKey = 'getUser(1)'
// Prefetch first
storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
await Promise.all(
storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
)
// No subscription from prefetch
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
// Now create a real subscription via initiate
const promise = storeRef.store.dispatch(api.endpoints.getUser.initiate(1))
// Should have 1 subscription from the initiate call
expect(getSubscriptionCount(queryCacheKey)).toBe(1)
// Unsubscribe
promise.unsubscribe()
// Subscription should be cleaned up
expect(getSubscriptionCount(queryCacheKey)).toBe(0)
})
})
})