| 1 | import { createApi, setupListeners } from '@reduxjs/toolkit/query/react'
|
|---|
| 2 | import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|---|
| 3 | import { delay } from 'msw'
|
|---|
| 4 | import { setupApiStore } from '../../tests/utils/helpers'
|
|---|
| 5 |
|
|---|
| 6 | // Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
|
|---|
| 7 | // This can be used to test how many renders happen due to data changes or
|
|---|
| 8 | // the refetching behavior of components.
|
|---|
| 9 | let amount = 0
|
|---|
| 10 |
|
|---|
| 11 | const defaultApi = createApi({
|
|---|
| 12 | baseQuery: async (arg: any) => {
|
|---|
| 13 | await delay(150)
|
|---|
| 14 | if ('amount' in arg?.body) {
|
|---|
| 15 | amount += 1
|
|---|
| 16 | }
|
|---|
| 17 | return {
|
|---|
| 18 | data: arg?.body
|
|---|
| 19 | ? { ...arg.body, ...(amount ? { amount } : {}) }
|
|---|
| 20 | : undefined,
|
|---|
| 21 | }
|
|---|
| 22 | },
|
|---|
| 23 | endpoints: (build) => ({
|
|---|
| 24 | getIncrementedAmount: build.query<any, void>({
|
|---|
| 25 | query: () => ({
|
|---|
| 26 | url: '',
|
|---|
| 27 | body: {
|
|---|
| 28 | amount,
|
|---|
| 29 | },
|
|---|
| 30 | }),
|
|---|
| 31 | }),
|
|---|
| 32 | }),
|
|---|
| 33 | refetchOnFocus: true,
|
|---|
| 34 | refetchOnReconnect: true,
|
|---|
| 35 | })
|
|---|
| 36 |
|
|---|
| 37 | const storeRef = setupApiStore(defaultApi)
|
|---|
| 38 |
|
|---|
| 39 | const getIncrementedAmountState = () =>
|
|---|
| 40 | storeRef.store.getState().api.queries['getIncrementedAmount(undefined)']
|
|---|
| 41 |
|
|---|
| 42 | afterEach(() => {
|
|---|
| 43 | amount = 0
|
|---|
| 44 | })
|
|---|
| 45 |
|
|---|
| 46 | describe('refetchOnFocus tests', () => {
|
|---|
| 47 | test('useQuery hook respects refetchOnFocus: true when set in createApi options', async () => {
|
|---|
| 48 | let data, isLoading, isFetching
|
|---|
| 49 |
|
|---|
| 50 | function User() {
|
|---|
| 51 | ;({ data, isFetching, isLoading } =
|
|---|
| 52 | defaultApi.endpoints.getIncrementedAmount.useQuery())
|
|---|
| 53 | return (
|
|---|
| 54 | <div>
|
|---|
| 55 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 56 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 57 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 58 | </div>
|
|---|
| 59 | )
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 63 |
|
|---|
| 64 | await waitFor(() =>
|
|---|
| 65 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 66 | )
|
|---|
| 67 | await waitFor(() =>
|
|---|
| 68 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 69 | )
|
|---|
| 70 | await waitFor(() =>
|
|---|
| 71 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 72 | )
|
|---|
| 73 |
|
|---|
| 74 | fireEvent.focus(window)
|
|---|
| 75 |
|
|---|
| 76 | await waitFor(() =>
|
|---|
| 77 | expect(screen.getByTestId('amount').textContent).toBe('2'),
|
|---|
| 78 | )
|
|---|
| 79 | })
|
|---|
| 80 |
|
|---|
| 81 | test('useQuery hook respects refetchOnFocus: false from a hook and overrides createApi defaults', async () => {
|
|---|
| 82 | let data, isLoading, isFetching
|
|---|
| 83 |
|
|---|
| 84 | function User() {
|
|---|
| 85 | ;({ data, isFetching, isLoading } =
|
|---|
| 86 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 87 | refetchOnFocus: false,
|
|---|
| 88 | }))
|
|---|
| 89 | return (
|
|---|
| 90 | <div>
|
|---|
| 91 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 92 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 93 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 94 | </div>
|
|---|
| 95 | )
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 99 |
|
|---|
| 100 | await waitFor(() =>
|
|---|
| 101 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 102 | )
|
|---|
| 103 | await waitFor(() =>
|
|---|
| 104 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 105 | )
|
|---|
| 106 | await waitFor(() =>
|
|---|
| 107 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 108 | )
|
|---|
| 109 |
|
|---|
| 110 | fireEvent.focus(window)
|
|---|
| 111 |
|
|---|
| 112 | await waitFor(() =>
|
|---|
| 113 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 114 | )
|
|---|
| 115 | })
|
|---|
| 116 |
|
|---|
| 117 | test('useQuery hook prefers refetchOnFocus: true when multiple components have different configurations', async () => {
|
|---|
| 118 | let data, isLoading, isFetching
|
|---|
| 119 |
|
|---|
| 120 | function User() {
|
|---|
| 121 | ;({ data, isFetching, isLoading } =
|
|---|
| 122 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 123 | refetchOnFocus: false,
|
|---|
| 124 | }))
|
|---|
| 125 | return (
|
|---|
| 126 | <div>
|
|---|
| 127 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 128 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 129 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 130 | </div>
|
|---|
| 131 | )
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | function UserWithRefetchTrue() {
|
|---|
| 135 | ;({ data, isFetching, isLoading } =
|
|---|
| 136 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 137 | refetchOnFocus: true,
|
|---|
| 138 | }))
|
|---|
| 139 | return <div />
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | render(
|
|---|
| 143 | <div>
|
|---|
| 144 | <User />
|
|---|
| 145 | <UserWithRefetchTrue />
|
|---|
| 146 | </div>,
|
|---|
| 147 | { wrapper: storeRef.wrapper },
|
|---|
| 148 | )
|
|---|
| 149 |
|
|---|
| 150 | await waitFor(() =>
|
|---|
| 151 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 152 | )
|
|---|
| 153 | await waitFor(() =>
|
|---|
| 154 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 155 | )
|
|---|
| 156 | await waitFor(() =>
|
|---|
| 157 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 158 | )
|
|---|
| 159 |
|
|---|
| 160 | fireEvent.focus(window)
|
|---|
| 161 | expect(screen.getByTestId('isLoading').textContent).toBe('false')
|
|---|
| 162 | await waitFor(() =>
|
|---|
| 163 | expect(screen.getByTestId('isFetching').textContent).toBe('true'),
|
|---|
| 164 | )
|
|---|
| 165 | await waitFor(() =>
|
|---|
| 166 | expect(screen.getByTestId('isFetching').textContent).toBe('false'),
|
|---|
| 167 | )
|
|---|
| 168 | await waitFor(() =>
|
|---|
| 169 | expect(screen.getByTestId('amount').textContent).toBe('2'),
|
|---|
| 170 | )
|
|---|
| 171 | })
|
|---|
| 172 |
|
|---|
| 173 | test('useQuery hook cleans data if refetch without active subscribers', async () => {
|
|---|
| 174 | let data, isLoading, isFetching
|
|---|
| 175 |
|
|---|
| 176 | function User() {
|
|---|
| 177 | ;({ data, isFetching, isLoading } =
|
|---|
| 178 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 179 | refetchOnFocus: true,
|
|---|
| 180 | }))
|
|---|
| 181 | return (
|
|---|
| 182 | <div>
|
|---|
| 183 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 184 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 185 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 186 | </div>
|
|---|
| 187 | )
|
|---|
| 188 | }
|
|---|
| 189 |
|
|---|
| 190 | const { unmount } = render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 191 |
|
|---|
| 192 | await waitFor(() =>
|
|---|
| 193 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 194 | )
|
|---|
| 195 | await waitFor(() =>
|
|---|
| 196 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 197 | )
|
|---|
| 198 | await waitFor(() =>
|
|---|
| 199 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 200 | )
|
|---|
| 201 |
|
|---|
| 202 | unmount()
|
|---|
| 203 |
|
|---|
| 204 | expect(getIncrementedAmountState()).not.toBeUndefined()
|
|---|
| 205 |
|
|---|
| 206 | fireEvent.focus(window)
|
|---|
| 207 |
|
|---|
| 208 | await delay(1)
|
|---|
| 209 | expect(getIncrementedAmountState()).toBeUndefined()
|
|---|
| 210 | })
|
|---|
| 211 | })
|
|---|
| 212 |
|
|---|
| 213 | describe('refetchOnReconnect tests', () => {
|
|---|
| 214 | test('useQuery hook respects refetchOnReconnect: true when set in createApi options', async () => {
|
|---|
| 215 | let data, isLoading, isFetching
|
|---|
| 216 |
|
|---|
| 217 | function User() {
|
|---|
| 218 | ;({ data, isFetching, isLoading } =
|
|---|
| 219 | defaultApi.endpoints.getIncrementedAmount.useQuery())
|
|---|
| 220 | return (
|
|---|
| 221 | <div>
|
|---|
| 222 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 223 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 224 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 225 | </div>
|
|---|
| 226 | )
|
|---|
| 227 | }
|
|---|
| 228 |
|
|---|
| 229 | render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 230 |
|
|---|
| 231 | await waitFor(() =>
|
|---|
| 232 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 233 | )
|
|---|
| 234 | await waitFor(() =>
|
|---|
| 235 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 236 | )
|
|---|
| 237 | await waitFor(() =>
|
|---|
| 238 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 239 | )
|
|---|
| 240 |
|
|---|
| 241 | act(() => {
|
|---|
| 242 | window.dispatchEvent(new Event('offline'))
|
|---|
| 243 | window.dispatchEvent(new Event('online'))
|
|---|
| 244 | })
|
|---|
| 245 |
|
|---|
| 246 | await waitFor(() =>
|
|---|
| 247 | expect(screen.getByTestId('isFetching').textContent).toBe('true'),
|
|---|
| 248 | )
|
|---|
| 249 | await waitFor(() =>
|
|---|
| 250 | expect(screen.getByTestId('isFetching').textContent).toBe('false'),
|
|---|
| 251 | )
|
|---|
| 252 | await waitFor(() =>
|
|---|
| 253 | expect(screen.getByTestId('amount').textContent).toBe('2'),
|
|---|
| 254 | )
|
|---|
| 255 | })
|
|---|
| 256 |
|
|---|
| 257 | test('useQuery hook should not refetch when refetchOnReconnect: false from a hook and overrides createApi defaults', async () => {
|
|---|
| 258 | let data, isLoading, isFetching
|
|---|
| 259 |
|
|---|
| 260 | function User() {
|
|---|
| 261 | ;({ data, isFetching, isLoading } =
|
|---|
| 262 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 263 | refetchOnReconnect: false,
|
|---|
| 264 | }))
|
|---|
| 265 | return (
|
|---|
| 266 | <div>
|
|---|
| 267 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 268 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 269 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 270 | </div>
|
|---|
| 271 | )
|
|---|
| 272 | }
|
|---|
| 273 |
|
|---|
| 274 | render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 275 |
|
|---|
| 276 | await waitFor(() =>
|
|---|
| 277 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 278 | )
|
|---|
| 279 | await waitFor(() =>
|
|---|
| 280 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 281 | )
|
|---|
| 282 | await waitFor(() =>
|
|---|
| 283 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 284 | )
|
|---|
| 285 |
|
|---|
| 286 | act(() => {
|
|---|
| 287 | window.dispatchEvent(new Event('offline'))
|
|---|
| 288 | window.dispatchEvent(new Event('online'))
|
|---|
| 289 | })
|
|---|
| 290 | expect(screen.getByTestId('isFetching').textContent).toBe('false')
|
|---|
| 291 | await waitFor(() =>
|
|---|
| 292 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 293 | )
|
|---|
| 294 | })
|
|---|
| 295 |
|
|---|
| 296 | test('useQuery hook prefers refetchOnReconnect: true when multiple components have different configurations', async () => {
|
|---|
| 297 | let data, isLoading, isFetching
|
|---|
| 298 |
|
|---|
| 299 | function User() {
|
|---|
| 300 | ;({ data, isFetching, isLoading } =
|
|---|
| 301 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 302 | refetchOnReconnect: false,
|
|---|
| 303 | }))
|
|---|
| 304 | return (
|
|---|
| 305 | <div>
|
|---|
| 306 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 307 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 308 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 309 | </div>
|
|---|
| 310 | )
|
|---|
| 311 | }
|
|---|
| 312 |
|
|---|
| 313 | function UserWithRefetchTrue() {
|
|---|
| 314 | ;({ data, isFetching, isLoading } =
|
|---|
| 315 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 316 | refetchOnReconnect: true,
|
|---|
| 317 | }))
|
|---|
| 318 | return <div />
|
|---|
| 319 | }
|
|---|
| 320 |
|
|---|
| 321 | render(
|
|---|
| 322 | <div>
|
|---|
| 323 | <User />
|
|---|
| 324 | <UserWithRefetchTrue />
|
|---|
| 325 | </div>,
|
|---|
| 326 | { wrapper: storeRef.wrapper },
|
|---|
| 327 | )
|
|---|
| 328 |
|
|---|
| 329 | await waitFor(() =>
|
|---|
| 330 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 331 | )
|
|---|
| 332 | await waitFor(() =>
|
|---|
| 333 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 334 | )
|
|---|
| 335 | await waitFor(() =>
|
|---|
| 336 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 337 | )
|
|---|
| 338 |
|
|---|
| 339 | act(() => {
|
|---|
| 340 | window.dispatchEvent(new Event('offline'))
|
|---|
| 341 | window.dispatchEvent(new Event('online'))
|
|---|
| 342 | })
|
|---|
| 343 |
|
|---|
| 344 | await waitFor(() =>
|
|---|
| 345 | expect(screen.getByTestId('isFetching').textContent).toBe('true'),
|
|---|
| 346 | )
|
|---|
| 347 | await waitFor(() =>
|
|---|
| 348 | expect(screen.getByTestId('isFetching').textContent).toBe('false'),
|
|---|
| 349 | )
|
|---|
| 350 | await waitFor(() =>
|
|---|
| 351 | expect(screen.getByTestId('amount').textContent).toBe('2'),
|
|---|
| 352 | )
|
|---|
| 353 | })
|
|---|
| 354 | })
|
|---|
| 355 |
|
|---|
| 356 | describe('customListenersHandler', () => {
|
|---|
| 357 | const storeRef = setupApiStore(defaultApi, undefined, {
|
|---|
| 358 | withoutListeners: true,
|
|---|
| 359 | })
|
|---|
| 360 |
|
|---|
| 361 | test('setupListeners accepts a custom callback and executes it', async () => {
|
|---|
| 362 | const consoleSpy = vi.spyOn(console, 'log')
|
|---|
| 363 | consoleSpy.mockImplementation((...args: any[]) => {
|
|---|
| 364 | // console.info(...args)
|
|---|
| 365 | })
|
|---|
| 366 | const dispatchSpy = vi.spyOn(storeRef.store, 'dispatch')
|
|---|
| 367 |
|
|---|
| 368 | let unsubscribe = () => {}
|
|---|
| 369 | unsubscribe = setupListeners(
|
|---|
| 370 | storeRef.store.dispatch,
|
|---|
| 371 | (dispatch, actions) => {
|
|---|
| 372 | const handleOnline = () =>
|
|---|
| 373 | dispatch(defaultApi.internalActions.onOnline())
|
|---|
| 374 | window.addEventListener('online', handleOnline, false)
|
|---|
| 375 | console.log('setup!')
|
|---|
| 376 | return () => {
|
|---|
| 377 | window.removeEventListener('online', handleOnline)
|
|---|
| 378 | console.log('cleanup!')
|
|---|
| 379 | }
|
|---|
| 380 | },
|
|---|
| 381 | )
|
|---|
| 382 |
|
|---|
| 383 | await delay(150)
|
|---|
| 384 |
|
|---|
| 385 | let data, isLoading, isFetching
|
|---|
| 386 |
|
|---|
| 387 | function User() {
|
|---|
| 388 | ;({ data, isFetching, isLoading } =
|
|---|
| 389 | defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, {
|
|---|
| 390 | refetchOnReconnect: true,
|
|---|
| 391 | }))
|
|---|
| 392 | return (
|
|---|
| 393 | <div>
|
|---|
| 394 | <div data-testid="isLoading">{String(isLoading)}</div>
|
|---|
| 395 | <div data-testid="isFetching">{String(isFetching)}</div>
|
|---|
| 396 | <div data-testid="amount">{String(data?.amount)}</div>
|
|---|
| 397 | </div>
|
|---|
| 398 | )
|
|---|
| 399 | }
|
|---|
| 400 |
|
|---|
| 401 | render(<User />, { wrapper: storeRef.wrapper })
|
|---|
| 402 |
|
|---|
| 403 | expect(consoleSpy).toHaveBeenCalledWith('setup!')
|
|---|
| 404 |
|
|---|
| 405 | await waitFor(() =>
|
|---|
| 406 | expect(screen.getByTestId('isLoading').textContent).toBe('true'),
|
|---|
| 407 | )
|
|---|
| 408 | await waitFor(() =>
|
|---|
| 409 | expect(screen.getByTestId('isLoading').textContent).toBe('false'),
|
|---|
| 410 | )
|
|---|
| 411 | await waitFor(() =>
|
|---|
| 412 | expect(screen.getByTestId('amount').textContent).toBe('1'),
|
|---|
| 413 | )
|
|---|
| 414 |
|
|---|
| 415 | act(() => {
|
|---|
| 416 | window.dispatchEvent(new Event('offline'))
|
|---|
| 417 | window.dispatchEvent(new Event('online'))
|
|---|
| 418 | })
|
|---|
| 419 | expect(dispatchSpy).toHaveBeenCalled()
|
|---|
| 420 |
|
|---|
| 421 | // Ignore RTKQ middleware internal data calls
|
|---|
| 422 | const mockCallsWithoutInternals = dispatchSpy.mock.calls.filter((call) => {
|
|---|
| 423 | const type = (call[0] as any)?.type ?? ''
|
|---|
| 424 | const reIsInternal = /internal/i
|
|---|
| 425 | return !reIsInternal.test(type)
|
|---|
| 426 | })
|
|---|
| 427 |
|
|---|
| 428 | expect(
|
|---|
| 429 | defaultApi.internalActions.onOnline.match(
|
|---|
| 430 | mockCallsWithoutInternals[1][0] as any,
|
|---|
| 431 | ),
|
|---|
| 432 | ).toBe(true)
|
|---|
| 433 |
|
|---|
| 434 | await waitFor(() =>
|
|---|
| 435 | expect(screen.getByTestId('isFetching').textContent).toBe('true'),
|
|---|
| 436 | )
|
|---|
| 437 | await waitFor(() =>
|
|---|
| 438 | expect(screen.getByTestId('isFetching').textContent).toBe('false'),
|
|---|
| 439 | )
|
|---|
| 440 | await waitFor(() =>
|
|---|
| 441 | expect(screen.getByTestId('amount').textContent).toBe('2'),
|
|---|
| 442 | )
|
|---|
| 443 |
|
|---|
| 444 | unsubscribe()
|
|---|
| 445 | expect(consoleSpy).toHaveBeenCalledWith('cleanup!')
|
|---|
| 446 | })
|
|---|
| 447 | })
|
|---|