import * as React from 'react' import { createApi, setupListeners } from '@reduxjs/toolkit/query/react' import { act, fireEvent, render, waitFor, screen } from '@testing-library/react' import { setupApiStore, waitMs } from './helpers' import { delay } from '../../utils' // 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 waitMs() 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) let 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') ) await act(async () => { fireEvent.focus(window) }) await waitMs() 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') ) act(() => { fireEvent.focus(window) }) await waitMs() 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') ) act(() => { 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() await act(async () => { 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 = jest.spyOn(console, 'log') consoleSpy.mockImplementation((...args) => { // console.info(...args) }) const dispatchSpy = jest.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 waitMs() 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_probeSubscription` calls const mockCallsWithoutInternals = dispatchSpy.mock.calls.filter( (call) => !(call[0] as any)?.type?.includes('internal') ) 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!') }) })