import { createApi } from '@reduxjs/toolkit/query/react' import { act, getByTestId, render, screen, waitFor, } from '@testing-library/react' import { delay } from 'msw' import { vi } from 'vitest' import { setupApiStore } from '../../tests/utils/helpers' describe('fixedCacheKey', () => { const onNewCacheEntry = vi.fn() const api = createApi({ async baseQuery(arg: string | Promise) { return { data: await arg } }, endpoints: (build) => ({ send: build.mutation>({ query: (arg) => arg, }), }), }) const storeRef = setupApiStore(api) function Component({ name, fixedCacheKey, value = name, }: { name: string fixedCacheKey?: string value?: string | Promise }) { const [trigger, result] = api.endpoints.send.useMutation({ fixedCacheKey }) return (
{result.status}
{result.data}
{String(result.originalArgs)}
) } test('two mutations without `fixedCacheKey` do not influence each other', async () => { render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => expect(getByTestId(c1, 'status').textContent).toBe('fulfilled'), ) expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') }) test('two mutations with the same `fixedCacheKey` do influence each other', async () => { render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => { expect(getByTestId(c1, 'status').textContent).toBe('fulfilled') expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') expect(getByTestId(c2, 'data').textContent).toBe('C1') }) // test reset from the other component act(() => { getByTestId(c2, 'reset').click() }) await waitFor(() => { expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c1, 'data').textContent).toBe('') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'data').textContent).toBe('') }) }) test('resetting from the component that triggered the mutation resets for each shared result', async () => { render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') const c3 = screen.getByTestId('C3') const c4 = screen.getByTestId('C4') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') expect(getByTestId(c3, 'status').textContent).toBe('uninitialized') expect(getByTestId(c4, 'status').textContent).toBe('uninitialized') // trigger with a component using the first cache key act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => expect(getByTestId(c1, 'status').textContent).toBe('fulfilled'), ) // the components with the first cache key should be affected expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') expect(getByTestId(c2, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') // the components with the second cache key should be unaffected expect(getByTestId(c3, 'data').textContent).toBe('') expect(getByTestId(c3, 'status').textContent).toBe('uninitialized') expect(getByTestId(c4, 'data').textContent).toBe('') expect(getByTestId(c4, 'status').textContent).toBe('uninitialized') // trigger with a component using the second cache key act(() => { getByTestId(c3, 'trigger').click() }) await waitFor(() => expect(getByTestId(c3, 'status').textContent).toBe('fulfilled'), ) // the components with the first cache key should be unaffected await waitFor(() => { expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') expect(getByTestId(c2, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') // the component with the second cache key should be affected expect(getByTestId(c3, 'data').textContent).toBe('C3') expect(getByTestId(c3, 'status').textContent).toBe('fulfilled') expect(getByTestId(c4, 'data').textContent).toBe('C3') expect(getByTestId(c4, 'status').textContent).toBe('fulfilled') }) // test reset from the component that triggered the mutation for the first cache key act(() => { getByTestId(c1, 'reset').click() }) await waitFor(() => { // the components with the first cache key should be affected expect(getByTestId(c1, 'data').textContent).toBe('') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'data').textContent).toBe('') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') // the components with the second cache key should be unaffected expect(getByTestId(c3, 'data').textContent).toBe('C3') expect(getByTestId(c3, 'status').textContent).toBe('fulfilled') expect(getByTestId(c4, 'data').textContent).toBe('C3') expect(getByTestId(c4, 'status').textContent).toBe('fulfilled') }) }) test('two mutations with different `fixedCacheKey` do not influence each other', async () => { render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => expect(getByTestId(c1, 'status').textContent).toBe('fulfilled'), ) expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') }) test('unmounting and remounting keeps data intact', async () => { const { rerender } = render(, { wrapper: storeRef.wrapper, }) let c1 = screen.getByTestId('C1') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => expect(getByTestId(c1, 'status').textContent).toBe('fulfilled'), ) expect(getByTestId(c1, 'data').textContent).toBe('C1') rerender(
) expect(screen.queryByTestId('C1')).toBe(null) rerender() c1 = screen.getByTestId('C1') expect(getByTestId(c1, 'status').textContent).toBe('fulfilled') expect(getByTestId(c1, 'data').textContent).toBe('C1') }) test('(limitation) mutations using `fixedCacheKey` do not return `originalArgs`', async () => { render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') act(() => { getByTestId(c1, 'trigger').click() }) await waitFor(() => expect(getByTestId(c1, 'status').textContent).toBe('fulfilled'), ) expect(getByTestId(c1, 'data').textContent).toBe('C1') expect(getByTestId(c2, 'status').textContent).toBe('fulfilled') expect(getByTestId(c2, 'data').textContent).toBe('C1') }) test('a component without `fixedCacheKey` has `originalArgs`', async () => { render(, { wrapper: storeRef.wrapper, }) let c1 = screen.getByTestId('C1') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c1, 'originalArgs').textContent).toBe('undefined') await act(async () => { getByTestId(c1, 'trigger').click() await Promise.resolve() }) expect(getByTestId(c1, 'originalArgs').textContent).toBe('C1') }) test('a component with `fixedCacheKey` does never have `originalArgs`', async () => { render(, { wrapper: storeRef.wrapper, }) let c1 = screen.getByTestId('C1') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c1, 'originalArgs').textContent).toBe('undefined') await act(async () => { getByTestId(c1, 'trigger').click() }) expect(getByTestId(c1, 'originalArgs').textContent).toBe('undefined') }) test('using `fixedCacheKey` will always use the latest dispatched thunk, prevent races', async () => { let resolve1: (str: string) => void, resolve2: (str: string) => void const p1 = new Promise((resolve) => { resolve1 = resolve }) const p2 = new Promise((resolve) => { resolve2 = resolve }) render( <> , { wrapper: storeRef.wrapper }, ) const c1 = screen.getByTestId('C1') const c2 = screen.getByTestId('C2') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c2, 'status').textContent).toBe('uninitialized') await act(async () => { getByTestId(c1, 'trigger').click() await Promise.resolve() }) expect(getByTestId(c1, 'status').textContent).toBe('pending') expect(getByTestId(c1, 'data').textContent).toBe('') act(() => { getByTestId(c2, 'trigger').click() }) expect(getByTestId(c1, 'status').textContent).toBe('pending') expect(getByTestId(c1, 'data').textContent).toBe('') await act(async () => { resolve1!('this should not show up any more') await Promise.resolve() }) await delay(150) expect(getByTestId(c1, 'status').textContent).toBe('pending') expect(getByTestId(c1, 'data').textContent).toBe('') await act(async () => { resolve2!('this should be visible') await Promise.resolve() }) await delay(150) expect(getByTestId(c1, 'status').textContent).toBe('fulfilled') expect(getByTestId(c1, 'data').textContent).toBe('this should be visible') }) test('using fixedCacheKey should create a new cache entry', async () => { api.enhanceEndpoints({ endpoints: { send: { onCacheEntryAdded: (arg) => onNewCacheEntry(arg), }, }, }) render(, { wrapper: storeRef.wrapper, }) let c1 = screen.getByTestId('C1') expect(getByTestId(c1, 'status').textContent).toBe('uninitialized') expect(getByTestId(c1, 'originalArgs').textContent).toBe('undefined') await act(async () => { getByTestId(c1, 'trigger').click() await Promise.resolve() }) expect(onNewCacheEntry).toHaveBeenCalledWith('C1') api.enhanceEndpoints({ endpoints: { send: { onCacheEntryAdded: undefined, }, }, }) }) })