gs-port / node_modules /@reduxjs /toolkit /src /query /tests /buildMiddleware.test.tsx
Scribbler310's picture
feat: enhance dashboard
c2b7eb3 verified
import { createApi } from '@reduxjs/toolkit/query'
import { delay } from 'msw'
import { actionsReducer, setupApiStore } from '../../tests/utils/helpers'
import { vi } from 'vitest'
const baseQuery = (args?: any) => ({ data: args })
const api = createApi({
baseQuery,
tagTypes: ['Banana', 'Bread'],
endpoints: (build) => ({
getBanana: build.query<unknown, number>({
query(id) {
return { url: `banana/${id}` }
},
providesTags: ['Banana'],
}),
getBananas: build.query<unknown, void>({
query() {
return { url: 'bananas' }
},
providesTags: ['Banana'],
}),
getBread: build.query<unknown, number>({
query(id) {
return { url: `bread/${id}` }
},
providesTags: ['Bread'],
}),
invalidateFruit: build.mutation({
query: (fruit?: 'Banana' | 'Bread' | null) => ({
url: `invalidate/fruit/${fruit || ''}`,
}),
invalidatesTags(result, error, arg) {
return [arg]
},
}),
}),
})
const { getBanana, getBread, invalidateFruit } = api.endpoints
const storeRef = setupApiStore(api, {
...actionsReducer,
})
it('invalidates the specified tags', async () => {
await storeRef.store.dispatch(getBanana.initiate(1))
expect(storeRef.store.getState().actions).toMatchSequence(
api.internalActions.middlewareRegistered.match,
getBanana.matchPending,
getBanana.matchFulfilled,
)
await storeRef.store.dispatch(api.util.invalidateTags(['Banana', 'Bread']))
// Slight pause to let the middleware run and such
await delay(20)
const firstSequence = [
api.internalActions.middlewareRegistered.match,
getBanana.matchPending,
getBanana.matchFulfilled,
api.util.invalidateTags.match,
getBanana.matchPending,
getBanana.matchFulfilled,
]
expect(storeRef.store.getState().actions).toMatchSequence(...firstSequence)
await storeRef.store.dispatch(getBread.initiate(1))
await storeRef.store.dispatch(api.util.invalidateTags([{ type: 'Bread' }]))
await delay(20)
expect(storeRef.store.getState().actions).toMatchSequence(
...firstSequence,
getBread.matchPending,
getBread.matchFulfilled,
api.util.invalidateTags.match,
getBread.matchPending,
getBread.matchFulfilled,
)
})
it('invalidates tags correctly when null or undefined are provided as tags', async () => {
await storeRef.store.dispatch(getBanana.initiate(1))
await storeRef.store.dispatch(
api.util.invalidateTags([undefined, null, 'Banana']),
)
// Slight pause to let the middleware run and such
await delay(20)
const apiActions = [
api.internalActions.middlewareRegistered.match,
getBanana.matchPending,
getBanana.matchFulfilled,
api.util.invalidateTags.match,
getBanana.matchPending,
getBanana.matchFulfilled,
]
expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
})
it.each([
{
tags: [undefined, null, 'Bread'] as Parameters<
typeof api.util.invalidateTags
>['0'],
},
{ tags: [undefined, null] },
{ tags: [] },
])(
'does not invalidate with tags=$tags if no query matches',
async ({ tags }) => {
await storeRef.store.dispatch(getBanana.initiate(1))
await storeRef.store.dispatch(api.util.invalidateTags(tags))
// Slight pause to let the middleware run and such
await delay(20)
const apiActions = [
api.internalActions.middlewareRegistered.match,
getBanana.matchPending,
getBanana.matchFulfilled,
api.util.invalidateTags.match,
]
expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
},
)
it.each([
{ mutationArg: 'Bread' as 'Bread' | null | undefined },
{ mutationArg: undefined },
{ mutationArg: null },
])(
'does not invalidate queries when a mutation with tags=[$mutationArg] runs and does not match anything',
async ({ mutationArg }) => {
await storeRef.store.dispatch(getBanana.initiate(1))
await storeRef.store.dispatch(invalidateFruit.initiate(mutationArg))
// Slight pause to let the middleware run and such
await delay(20)
const apiActions = [
api.internalActions.middlewareRegistered.match,
getBanana.matchPending,
getBanana.matchFulfilled,
invalidateFruit.matchPending,
invalidateFruit.matchFulfilled,
]
expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
},
)
it('correctly stringifies subscription state and dispatches subscriptionsUpdated', async () => {
// Create a fresh store for this test to avoid interference
const testStoreRef = setupApiStore(
api,
{
...actionsReducer,
},
{ withoutListeners: true },
)
// Start multiple subscriptions
const subscription1 = testStoreRef.store.dispatch(
getBanana.initiate(1, {
subscriptionOptions: { pollingInterval: 1000 },
}),
)
const subscription2 = testStoreRef.store.dispatch(
getBanana.initiate(2, {
subscriptionOptions: { refetchOnFocus: true },
}),
)
const subscription3 = testStoreRef.store.dispatch(
api.endpoints.getBananas.initiate(),
)
// Wait for the subscriptions to be established
await Promise.all([subscription1, subscription2, subscription3])
// Wait for the subscription sync timer (500ms + buffer)
await delay(600)
// Check the final subscription state in the store
const finalState = testStoreRef.store.getState()
const subscriptionState = finalState[api.reducerPath].subscriptions
// Should have subscriptions for getBanana(1), getBanana(2), and getBananas()
expect(subscriptionState).toMatchObject({
'getBanana(1)': {
[subscription1.requestId]: { pollingInterval: 1000 },
},
'getBanana(2)': {
[subscription2.requestId]: { refetchOnFocus: true },
},
'getBananas(undefined)': {
[subscription3.requestId]: {},
},
})
// Verify the subscription entries have the expected structure
expect(Object.keys(subscriptionState)).toHaveLength(3)
expect(subscriptionState['getBanana(1)']?.[subscription1.requestId]).toEqual({
pollingInterval: 1000,
})
expect(subscriptionState['getBanana(2)']?.[subscription2.requestId]).toEqual({
refetchOnFocus: true,
})
expect(
subscriptionState['getBananas(undefined)']?.[subscription3.requestId],
).toEqual({})
})
it('does not leak subscription state between multiple stores using the same API instance (SSR scenario)', async () => {
vi.useFakeTimers()
// Simulate SSR: create API once at module level
const sharedApi = createApi({
baseQuery: (args?: any) => ({ data: args }),
tagTypes: ['Test'],
endpoints: (build) => ({
getTest: build.query<unknown, number>({
query(id) {
return { url: `test/${id}` }
},
}),
}),
})
// Create first store (simulating first SSR request)
const store1Ref = setupApiStore(sharedApi, {}, { withoutListeners: true })
// Add subscription in store1
const sub1 = store1Ref.store.dispatch(
sharedApi.endpoints.getTest.initiate(1, {
subscriptionOptions: { pollingInterval: 1000 },
}),
)
vi.advanceTimersByTime(10)
await sub1
// Wait for subscription sync (500ms + buffer)
vi.advanceTimersByTime(600)
// Verify store1 has the subscription
const store1SubscriptionSelectors = store1Ref.store.dispatch(
sharedApi.internalActions.internal_getRTKQSubscriptions(),
) as any
const store1InternalSubs = store1SubscriptionSelectors.getSubscriptions()
expect(store1InternalSubs.size).toBe(1)
// Create second store (simulating second SSR request)
const store2Ref = setupApiStore(sharedApi, {}, { withoutListeners: true })
// Check subscriptions via internal action
const store2SubscriptionSelectors = store2Ref.store.dispatch(
sharedApi.internalActions.internal_getRTKQSubscriptions(),
) as any
const store2InternalSubs = store2SubscriptionSelectors.getSubscriptions()
expect(store2InternalSubs.size).toBe(0)
})