source: node_modules/@reduxjs/toolkit/src/query/tests/buildMiddleware.test.tsx

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 7.8 KB
Line 
1import { createApi } from '@reduxjs/toolkit/query'
2import { delay } from 'msw'
3import { actionsReducer, setupApiStore } from '../../tests/utils/helpers'
4import { vi } from 'vitest'
5
6const baseQuery = (args?: any) => ({ data: args })
7const api = createApi({
8 baseQuery,
9 tagTypes: ['Banana', 'Bread'],
10 endpoints: (build) => ({
11 getBanana: build.query<unknown, number>({
12 query(id) {
13 return { url: `banana/${id}` }
14 },
15 providesTags: ['Banana'],
16 }),
17 getBananas: build.query<unknown, void>({
18 query() {
19 return { url: 'bananas' }
20 },
21 providesTags: ['Banana'],
22 }),
23 getBread: build.query<unknown, number>({
24 query(id) {
25 return { url: `bread/${id}` }
26 },
27 providesTags: ['Bread'],
28 }),
29 invalidateFruit: build.mutation({
30 query: (fruit?: 'Banana' | 'Bread' | null) => ({
31 url: `invalidate/fruit/${fruit || ''}`,
32 }),
33 invalidatesTags(result, error, arg) {
34 return [arg]
35 },
36 }),
37 }),
38})
39const { getBanana, getBread, invalidateFruit } = api.endpoints
40
41const storeRef = setupApiStore(api, {
42 ...actionsReducer,
43})
44
45it('invalidates the specified tags', async () => {
46 await storeRef.store.dispatch(getBanana.initiate(1))
47 expect(storeRef.store.getState().actions).toMatchSequence(
48 api.internalActions.middlewareRegistered.match,
49 getBanana.matchPending,
50 getBanana.matchFulfilled,
51 )
52
53 await storeRef.store.dispatch(api.util.invalidateTags(['Banana', 'Bread']))
54
55 // Slight pause to let the middleware run and such
56 await delay(20)
57
58 const firstSequence = [
59 api.internalActions.middlewareRegistered.match,
60 getBanana.matchPending,
61 getBanana.matchFulfilled,
62 api.util.invalidateTags.match,
63 getBanana.matchPending,
64 getBanana.matchFulfilled,
65 ]
66 expect(storeRef.store.getState().actions).toMatchSequence(...firstSequence)
67
68 await storeRef.store.dispatch(getBread.initiate(1))
69 await storeRef.store.dispatch(api.util.invalidateTags([{ type: 'Bread' }]))
70
71 await delay(20)
72
73 expect(storeRef.store.getState().actions).toMatchSequence(
74 ...firstSequence,
75 getBread.matchPending,
76 getBread.matchFulfilled,
77 api.util.invalidateTags.match,
78 getBread.matchPending,
79 getBread.matchFulfilled,
80 )
81})
82
83it('invalidates tags correctly when null or undefined are provided as tags', async () => {
84 await storeRef.store.dispatch(getBanana.initiate(1))
85 await storeRef.store.dispatch(
86 api.util.invalidateTags([undefined, null, 'Banana']),
87 )
88
89 // Slight pause to let the middleware run and such
90 await delay(20)
91
92 const apiActions = [
93 api.internalActions.middlewareRegistered.match,
94 getBanana.matchPending,
95 getBanana.matchFulfilled,
96 api.util.invalidateTags.match,
97 getBanana.matchPending,
98 getBanana.matchFulfilled,
99 ]
100
101 expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
102})
103
104it.each([
105 {
106 tags: [undefined, null, 'Bread'] as Parameters<
107 typeof api.util.invalidateTags
108 >['0'],
109 },
110 { tags: [undefined, null] },
111 { tags: [] },
112])(
113 'does not invalidate with tags=$tags if no query matches',
114 async ({ tags }) => {
115 await storeRef.store.dispatch(getBanana.initiate(1))
116 await storeRef.store.dispatch(api.util.invalidateTags(tags))
117
118 // Slight pause to let the middleware run and such
119 await delay(20)
120
121 const apiActions = [
122 api.internalActions.middlewareRegistered.match,
123 getBanana.matchPending,
124 getBanana.matchFulfilled,
125 api.util.invalidateTags.match,
126 ]
127
128 expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
129 },
130)
131
132it.each([
133 { mutationArg: 'Bread' as 'Bread' | null | undefined },
134 { mutationArg: undefined },
135 { mutationArg: null },
136])(
137 'does not invalidate queries when a mutation with tags=[$mutationArg] runs and does not match anything',
138 async ({ mutationArg }) => {
139 await storeRef.store.dispatch(getBanana.initiate(1))
140 await storeRef.store.dispatch(invalidateFruit.initiate(mutationArg))
141
142 // Slight pause to let the middleware run and such
143 await delay(20)
144
145 const apiActions = [
146 api.internalActions.middlewareRegistered.match,
147 getBanana.matchPending,
148 getBanana.matchFulfilled,
149 invalidateFruit.matchPending,
150 invalidateFruit.matchFulfilled,
151 ]
152
153 expect(storeRef.store.getState().actions).toMatchSequence(...apiActions)
154 },
155)
156
157it('correctly stringifies subscription state and dispatches subscriptionsUpdated', async () => {
158 // Create a fresh store for this test to avoid interference
159 const testStoreRef = setupApiStore(
160 api,
161 {
162 ...actionsReducer,
163 },
164 { withoutListeners: true },
165 )
166
167 // Start multiple subscriptions
168 const subscription1 = testStoreRef.store.dispatch(
169 getBanana.initiate(1, {
170 subscriptionOptions: { pollingInterval: 1000 },
171 }),
172 )
173 const subscription2 = testStoreRef.store.dispatch(
174 getBanana.initiate(2, {
175 subscriptionOptions: { refetchOnFocus: true },
176 }),
177 )
178 const subscription3 = testStoreRef.store.dispatch(
179 api.endpoints.getBananas.initiate(),
180 )
181
182 // Wait for the subscriptions to be established
183 await Promise.all([subscription1, subscription2, subscription3])
184
185 // Wait for the subscription sync timer (500ms + buffer)
186 await delay(600)
187
188 // Check the final subscription state in the store
189 const finalState = testStoreRef.store.getState()
190 const subscriptionState = finalState[api.reducerPath].subscriptions
191
192 // Should have subscriptions for getBanana(1), getBanana(2), and getBananas()
193 expect(subscriptionState).toMatchObject({
194 'getBanana(1)': {
195 [subscription1.requestId]: { pollingInterval: 1000 },
196 },
197 'getBanana(2)': {
198 [subscription2.requestId]: { refetchOnFocus: true },
199 },
200 'getBananas(undefined)': {
201 [subscription3.requestId]: {},
202 },
203 })
204
205 // Verify the subscription entries have the expected structure
206 expect(Object.keys(subscriptionState)).toHaveLength(3)
207 expect(subscriptionState['getBanana(1)']?.[subscription1.requestId]).toEqual({
208 pollingInterval: 1000,
209 })
210 expect(subscriptionState['getBanana(2)']?.[subscription2.requestId]).toEqual({
211 refetchOnFocus: true,
212 })
213 expect(
214 subscriptionState['getBananas(undefined)']?.[subscription3.requestId],
215 ).toEqual({})
216})
217
218it('does not leak subscription state between multiple stores using the same API instance (SSR scenario)', async () => {
219 vi.useFakeTimers()
220 // Simulate SSR: create API once at module level
221 const sharedApi = createApi({
222 baseQuery: (args?: any) => ({ data: args }),
223 tagTypes: ['Test'],
224 endpoints: (build) => ({
225 getTest: build.query<unknown, number>({
226 query(id) {
227 return { url: `test/${id}` }
228 },
229 }),
230 }),
231 })
232
233 // Create first store (simulating first SSR request)
234 const store1Ref = setupApiStore(sharedApi, {}, { withoutListeners: true })
235
236 // Add subscription in store1
237 const sub1 = store1Ref.store.dispatch(
238 sharedApi.endpoints.getTest.initiate(1, {
239 subscriptionOptions: { pollingInterval: 1000 },
240 }),
241 )
242 vi.advanceTimersByTime(10)
243 await sub1
244
245 // Wait for subscription sync (500ms + buffer)
246 vi.advanceTimersByTime(600)
247
248 // Verify store1 has the subscription
249 const store1SubscriptionSelectors = store1Ref.store.dispatch(
250 sharedApi.internalActions.internal_getRTKQSubscriptions(),
251 ) as any
252 const store1InternalSubs = store1SubscriptionSelectors.getSubscriptions()
253 expect(store1InternalSubs.size).toBe(1)
254
255 // Create second store (simulating second SSR request)
256 const store2Ref = setupApiStore(sharedApi, {}, { withoutListeners: true })
257
258 // Check subscriptions via internal action
259 const store2SubscriptionSelectors = store2Ref.store.dispatch(
260 sharedApi.internalActions.internal_getRTKQSubscriptions(),
261 ) as any
262
263 const store2InternalSubs = store2SubscriptionSelectors.getSubscriptions()
264
265 expect(store2InternalSubs.size).toBe(0)
266})
Note: See TracBrowser for help on using the repository browser.