Spaces:
Sleeping
Sleeping
| // tests for "cleanup-after-unsubscribe" behavior | |
| import React from 'react' | |
| import { createListenerMiddleware } from '@reduxjs/toolkit' | |
| import { createApi, QueryStatus } from '@reduxjs/toolkit/query/react' | |
| import { act, render, screen, waitFor } from '@testing-library/react' | |
| import { setupApiStore } from '../../tests/utils/helpers' | |
| import type { SubscriptionSelectors } from '../core/buildMiddleware/types' | |
| const api = createApi({ | |
| baseQuery: () => ({ data: 42 }), | |
| endpoints: (build) => ({ | |
| a: build.query<unknown, void>({ query: () => '' }), | |
| b: build.query<unknown, void>({ query: () => '' }), | |
| }), | |
| }) | |
| const storeRef = setupApiStore(api) | |
| const getSubStateA = () => storeRef.store.getState().api.queries['a(undefined)'] | |
| const getSubStateB = () => storeRef.store.getState().api.queries['b(undefined)'] | |
| function UsingA() { | |
| const { data } = api.endpoints.a.useQuery() | |
| return <>Result: {data as React.ReactNode} </> | |
| } | |
| function UsingB() { | |
| api.endpoints.b.useQuery() | |
| return <></> | |
| } | |
| function UsingAB() { | |
| api.endpoints.a.useQuery() | |
| api.endpoints.b.useQuery() | |
| return <></> | |
| } | |
| beforeAll(() => { | |
| vi.useFakeTimers({ shouldAdvanceTime: true }) | |
| }) | |
| test('data stays in store when component stays rendered', async () => { | |
| expect(getSubStateA()).toBeUndefined() | |
| render(<UsingA />, { wrapper: storeRef.wrapper }) | |
| await waitFor(() => | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled), | |
| ) | |
| vi.advanceTimersByTime(120_000) | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled) | |
| }) | |
| test('data is removed from store after 60 seconds', async () => { | |
| expect(getSubStateA()).toBeUndefined() | |
| const { unmount } = render(<UsingA />, { wrapper: storeRef.wrapper }) | |
| await waitFor(() => | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled), | |
| ) | |
| unmount() | |
| vi.advanceTimersByTime(59_000) | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled) | |
| vi.advanceTimersByTime(2000) | |
| expect(getSubStateA()).toBeUndefined() | |
| }) | |
| test('data stays in store when component stays rendered while data for another component is removed after it unmounted', async () => { | |
| expect(getSubStateA()).toBeUndefined() | |
| expect(getSubStateB()).toBeUndefined() | |
| const { rerender } = render( | |
| <> | |
| <UsingA /> | |
| <UsingB /> | |
| </>, | |
| { wrapper: storeRef.wrapper }, | |
| ) | |
| await waitFor(() => { | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled) | |
| expect(getSubStateB()?.status).toBe(QueryStatus.fulfilled) | |
| }) | |
| const statusA = getSubStateA() | |
| await act(async () => { | |
| rerender(<UsingA />) | |
| vi.advanceTimersByTime(10) | |
| }) | |
| vi.advanceTimersByTime(120_000) | |
| expect(getSubStateA()).toEqual(statusA) | |
| expect(getSubStateB()).toBeUndefined() | |
| }) | |
| test('data stays in store when one component requiring the data stays in the store', async () => { | |
| expect(getSubStateA()).toBeUndefined() | |
| expect(getSubStateB()).toBeUndefined() | |
| const { rerender } = render( | |
| <> | |
| <UsingA key="a" /> | |
| <UsingAB key="ab" /> | |
| </>, | |
| { wrapper: storeRef.wrapper }, | |
| ) | |
| await waitFor(() => { | |
| expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled) | |
| expect(getSubStateB()?.status).toBe(QueryStatus.fulfilled) | |
| }) | |
| const statusA = getSubStateA() | |
| const statusB = getSubStateB() | |
| await act(async () => { | |
| rerender(<UsingAB key="ab" />) | |
| vi.advanceTimersByTime(10) | |
| vi.runAllTimers() | |
| }) | |
| await act(async () => { | |
| vi.advanceTimersByTime(120000) | |
| vi.runAllTimers() | |
| }) | |
| expect(getSubStateA()).toEqual(statusA) | |
| expect(getSubStateB()).toEqual(statusB) | |
| }) | |
| test('Minimizes the number of subscription dispatches when multiple components ask for the same data', async () => { | |
| const listenerMiddleware = createListenerMiddleware() | |
| const storeRef = setupApiStore(api, undefined, { | |
| middleware: { | |
| concat: [listenerMiddleware.middleware], | |
| }, | |
| withoutTestLifecycles: true, | |
| }) | |
| const actionTypes: unknown[] = [] | |
| listenerMiddleware.startListening({ | |
| predicate: () => true, | |
| effect: (action) => { | |
| if ( | |
| action.type.includes('subscriptionsUpdated') || | |
| action.type.includes('internal_') | |
| ) { | |
| return | |
| } | |
| actionTypes.push(action.type) | |
| }, | |
| }) | |
| const { getSubscriptionCount } = storeRef.store.dispatch( | |
| api.internalActions.internal_getRTKQSubscriptions(), | |
| ) as unknown as SubscriptionSelectors | |
| const NUM_LIST_ITEMS = 1000 | |
| function ParentComponent() { | |
| const listItems = Array.from({ length: NUM_LIST_ITEMS }).map((_, i) => ( | |
| <UsingA key={i} /> | |
| )) | |
| return <>{listItems}</> | |
| } | |
| render(<ParentComponent />, { | |
| wrapper: storeRef.wrapper, | |
| }) | |
| await act(async () => { | |
| vi.advanceTimersByTime(10) | |
| vi.runAllTimers() | |
| }) | |
| await waitFor(() => { | |
| return screen.getAllByText(/42/).length > 0 | |
| }) | |
| expect(getSubscriptionCount('a(undefined)')).toBe(NUM_LIST_ITEMS) | |
| expect(actionTypes).toEqual([ | |
| 'api/config/middlewareRegistered', | |
| 'api/executeQuery/pending', | |
| 'api/executeQuery/fulfilled', | |
| ]) | |
| }, 25_000) | |