Spaces:
Running
Running
| 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<string>) { | |
| return { data: await arg } | |
| }, | |
| endpoints: (build) => ({ | |
| send: build.mutation<string, string | Promise<string>>({ | |
| query: (arg) => arg, | |
| }), | |
| }), | |
| }) | |
| const storeRef = setupApiStore(api) | |
| function Component({ | |
| name, | |
| fixedCacheKey, | |
| value = name, | |
| }: { | |
| name: string | |
| fixedCacheKey?: string | |
| value?: string | Promise<string> | |
| }) { | |
| const [trigger, result] = api.endpoints.send.useMutation({ fixedCacheKey }) | |
| return ( | |
| <div data-testid={name}> | |
| <div data-testid="status">{result.status}</div> | |
| <div data-testid="data">{result.data}</div> | |
| <div data-testid="originalArgs">{String(result.originalArgs)}</div> | |
| <button data-testid="trigger" onClick={() => trigger(value)}> | |
| trigger | |
| </button> | |
| <button data-testid="reset" onClick={result.reset}> | |
| reset | |
| </button> | |
| </div> | |
| ) | |
| } | |
| test('two mutations without `fixedCacheKey` do not influence each other', async () => { | |
| render( | |
| <> | |
| <Component name="C1" /> | |
| <Component name="C2" /> | |
| </>, | |
| { 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( | |
| <> | |
| <Component name="C1" fixedCacheKey="test" /> | |
| <Component name="C2" fixedCacheKey="test" /> | |
| </>, | |
| { 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( | |
| <> | |
| <Component name="C1" fixedCacheKey="test-A" /> | |
| <Component name="C2" fixedCacheKey="test-A" /> | |
| <Component name="C3" fixedCacheKey="test-B" /> | |
| <Component name="C4" fixedCacheKey="test-B" /> | |
| </>, | |
| { 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( | |
| <> | |
| <Component name="C1" fixedCacheKey="test" /> | |
| <Component name="C2" fixedCacheKey="toast" /> | |
| </>, | |
| { 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(<Component name="C1" fixedCacheKey="test" />, { | |
| 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(<div />) | |
| expect(screen.queryByTestId('C1')).toBe(null) | |
| rerender(<Component name="C1" fixedCacheKey="test" />) | |
| 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( | |
| <> | |
| <Component name="C1" fixedCacheKey="test" /> | |
| <Component name="C2" fixedCacheKey="test" /> | |
| </>, | |
| { 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(<Component name="C1" />, { | |
| 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(<Component name="C1" fixedCacheKey="test" />, { | |
| 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<string>((resolve) => { | |
| resolve1 = resolve | |
| }) | |
| const p2 = new Promise<string>((resolve) => { | |
| resolve2 = resolve | |
| }) | |
| render( | |
| <> | |
| <Component name="C1" fixedCacheKey="test" value={p1} /> | |
| <Component name="C2" fixedCacheKey="test" value={p2} /> | |
| </>, | |
| { 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(<Component name="C1" fixedCacheKey={'testKey'} />, { | |
| 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, | |
| }, | |
| }, | |
| }) | |
| }) | |
| }) | |