import { createApi, setupListeners } from '@reduxjs/toolkit/query/react' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { delay } from 'msw' import { setupApiStore } from '../../tests/utils/helpers' // Just setup a temporary in-memory counter for tests that `getIncrementedAmount`. // This can be used to test how many renders happen due to data changes or // the refetching behavior of components. let amount = 0 const defaultApi = createApi({ baseQuery: async (arg: any) => { await delay(150) if ('amount' in arg?.body) { amount += 1 } return { data: arg?.body ? { ...arg.body, ...(amount ? { amount } : {}) } : undefined, } }, endpoints: (build) => ({ getIncrementedAmount: build.query({ query: () => ({ url: '', body: { amount, }, }), }), }), refetchOnFocus: true, refetchOnReconnect: true, }) const storeRef = setupApiStore(defaultApi) const getIncrementedAmountState = () => storeRef.store.getState().api.queries['getIncrementedAmount(undefined)'] afterEach(() => { amount = 0 }) describe('refetchOnFocus tests', () => { test('useQuery hook respects refetchOnFocus: true when set in createApi options', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery()) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } render(, { wrapper: storeRef.wrapper }) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) fireEvent.focus(window) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('2'), ) }) test('useQuery hook respects refetchOnFocus: false from a hook and overrides createApi defaults', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnFocus: false, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } render(, { wrapper: storeRef.wrapper }) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) fireEvent.focus(window) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) }) test('useQuery hook prefers refetchOnFocus: true when multiple components have different configurations', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnFocus: false, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } function UserWithRefetchTrue() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnFocus: true, })) return
} render(
, { wrapper: storeRef.wrapper }, ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) fireEvent.focus(window) expect(screen.getByTestId('isLoading').textContent).toBe('false') await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('2'), ) }) test('useQuery hook cleans data if refetch without active subscribers', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnFocus: true, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } const { unmount } = render(, { wrapper: storeRef.wrapper }) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) unmount() expect(getIncrementedAmountState()).not.toBeUndefined() fireEvent.focus(window) await delay(1) expect(getIncrementedAmountState()).toBeUndefined() }) }) describe('refetchOnReconnect tests', () => { test('useQuery hook respects refetchOnReconnect: true when set in createApi options', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery()) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } render(, { wrapper: storeRef.wrapper }) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) act(() => { window.dispatchEvent(new Event('offline')) window.dispatchEvent(new Event('online')) }) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('2'), ) }) test('useQuery hook should not refetch when refetchOnReconnect: false from a hook and overrides createApi defaults', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnReconnect: false, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } render(, { wrapper: storeRef.wrapper }) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) act(() => { window.dispatchEvent(new Event('offline')) window.dispatchEvent(new Event('online')) }) expect(screen.getByTestId('isFetching').textContent).toBe('false') await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) }) test('useQuery hook prefers refetchOnReconnect: true when multiple components have different configurations', async () => { let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnReconnect: false, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } function UserWithRefetchTrue() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnReconnect: true, })) return
} render(
, { wrapper: storeRef.wrapper }, ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) act(() => { window.dispatchEvent(new Event('offline')) window.dispatchEvent(new Event('online')) }) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('2'), ) }) }) describe('customListenersHandler', () => { const storeRef = setupApiStore(defaultApi, undefined, { withoutListeners: true, }) test('setupListeners accepts a custom callback and executes it', async () => { const consoleSpy = vi.spyOn(console, 'log') consoleSpy.mockImplementation((...args: any[]) => { // console.info(...args) }) const dispatchSpy = vi.spyOn(storeRef.store, 'dispatch') let unsubscribe = () => {} unsubscribe = setupListeners( storeRef.store.dispatch, (dispatch, actions) => { const handleOnline = () => dispatch(defaultApi.internalActions.onOnline()) window.addEventListener('online', handleOnline, false) console.log('setup!') return () => { window.removeEventListener('online', handleOnline) console.log('cleanup!') } }, ) await delay(150) let data, isLoading, isFetching function User() { ;({ data, isFetching, isLoading } = defaultApi.endpoints.getIncrementedAmount.useQuery(undefined, { refetchOnReconnect: true, })) return (
{String(isLoading)}
{String(isFetching)}
{String(data?.amount)}
) } render(, { wrapper: storeRef.wrapper }) expect(consoleSpy).toHaveBeenCalledWith('setup!') await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isLoading').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('1'), ) act(() => { window.dispatchEvent(new Event('offline')) window.dispatchEvent(new Event('online')) }) expect(dispatchSpy).toHaveBeenCalled() // Ignore RTKQ middleware internal data calls const mockCallsWithoutInternals = dispatchSpy.mock.calls.filter((call) => { const type = (call[0] as any)?.type ?? '' const reIsInternal = /internal/i return !reIsInternal.test(type) }) expect( defaultApi.internalActions.onOnline.match( mockCallsWithoutInternals[1][0] as any, ), ).toBe(true) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'), ) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'), ) await waitFor(() => expect(screen.getByTestId('amount').textContent).toBe('2'), ) unsubscribe() expect(consoleSpy).toHaveBeenCalledWith('cleanup!') }) })