import * as React from 'react'; import { render, waitFor, screen, fireEvent } from '@testing-library/react'; import expect from 'expect'; import { QueryClient, useMutationState } from '@tanstack/react-query'; import { RaRecord } from '../types'; import { testDataProvider } from './testDataProvider'; import { useCreate } from './useCreate'; import { useGetList } from './useGetList'; import { CoreAdminContext } from '../core'; import { ErrorCase as ErrorCasePessimistic, SuccessCase as SuccessCasePessimistic, WithMiddlewaresSuccess as WithMiddlewaresSuccessPessimistic, WithMiddlewaresError as WithMiddlewaresErrorPessimistic, } from './useCreate.pessimistic.stories'; import { ErrorCase as ErrorCaseOptimistic, SuccessCase as SuccessCaseOptimistic, WithMiddlewaresSuccess as WithMiddlewaresSuccessOptimistic, WithMiddlewaresError as WithMiddlewaresErrorOptimistic, } from './useCreate.optimistic.stories'; import { ErrorCase as ErrorCaseUndoable, SuccessCase as SuccessCaseUndoable, WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable, WithMiddlewaresError as WithMiddlewaresErrorUndoable, } from './useCreate.undoable.stories'; import { Middleware, MutationMode, Params, InvalidateList, } from './useCreate.stories'; describe('useCreate', () => { it('returns a callback that can be used with create arguments', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const Dummy = () => { const [create] = useCreate(); localCreate = create; return ; }; render( ); localCreate('foo', { data: { bar: 'baz' } }); await waitFor(() => { expect(dataProvider.create).toHaveBeenCalledWith('foo', { data: { bar: 'baz' }, }); }); }); it('returns a callback that can be used with no arguments', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const Dummy = () => { const [create] = useCreate('foo', { data: { bar: 'baz' } }); localCreate = create; return ; }; render( ); localCreate(); await waitFor(() => { expect(dataProvider.create).toHaveBeenCalledWith('foo', { data: { bar: 'baz' }, }); }); }); it('uses a custom mutationFn with mutation middlewares', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); const customMutationFn = jest.fn(async params => ({ id: 1, title: params.data?.title, middlewareApplied: params.data?.middlewareApplied, })); let localCreate; let mutationData; const Dummy = () => { const [create, { data }] = useCreate(undefined, undefined, { mutationFn: customMutationFn, getMutateWithMiddlewares: mutate => async (resource, params) => mutate(resource, { ...params, data: { ...params.data, middlewareApplied: true, }, }), }); localCreate = create; mutationData = data; return ; }; render( ); localCreate('foo', { data: { title: 'Hello' } }); await waitFor(() => { expect(customMutationFn).toHaveBeenCalledWith({ resource: 'foo', data: { title: 'Hello', middlewareApplied: true, }, }); }); expect(dataProvider.create).not.toHaveBeenCalled(); await waitFor(() => { expect(mutationData).toEqual({ id: 1, title: 'Hello', middlewareApplied: true, }); }); }); it('uses the latest declaration time mutationMode', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); // This story uses the pessimistic mode by default render(); fireEvent.click(screen.getByText('Change mutation mode to optimistic')); fireEvent.click(screen.getByText('Create post')); // Should display the optimistic result right away if the change was handled await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('mutating')).toBeNull(); }); expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); }); it('uses the latest declaration time params', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); // This story sends the Hello World title by default render(); fireEvent.click(screen.getByText('Change params')); fireEvent.click(screen.getByText('Create post')); // Should have changed the title to Goodbye World await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Goodbye World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('mutating')).toBeNull(); }); expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Goodbye World')).not.toBeNull(); }); it('uses call time params over hook time params', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const Dummy = () => { const [create] = useCreate('foo', { data: { bar: 'baz' } }); localCreate = create; return ; }; render( ); localCreate('foo', { data: { foo: 456 } }); await waitFor(() => { expect(dataProvider.create).toHaveBeenCalledWith('foo', { data: { foo: 456 }, }); }); }); it('calls onSettled when provided in hook time options', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const onSettled = jest.fn(); const Dummy = () => { const [create] = useCreate( 'foo', { data: { bar: 'baz' } }, { onSettled } ); localCreate = create; return ; }; render( ); localCreate('foo', { data: { foo: 456 } }); await waitFor(() => { expect(onSettled).toHaveBeenCalled(); }); }); it('accepts a meta parameter', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const Dummy = () => { const [create] = useCreate(); localCreate = create; return ; }; render( ); localCreate('foo', { data: { bar: 'baz' }, meta: { hello: 'world' } }); await waitFor(() => { expect(dataProvider.create).toHaveBeenCalledWith('foo', { data: { bar: 'baz' }, meta: { hello: 'world' }, }); }); }); it('sets the mutationKey', async () => { const queryClient = new QueryClient(); queryClient.setMutationDefaults(['foo', 'create'], { meta: { hello: 'world' }, }); const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0 } } as any)), }); let localCreate; const Dummy = () => { const [create] = useCreate('foo'); localCreate = create; return ; }; const Observe = () => { const mutation = useMutationState({ filters: { mutationKey: ['foo', 'create'], }, }); return mutations: {mutation.length}; }; render( ); localCreate('foo', { data: { bar: 'baz' }, meta: { hello: 'world' } }); await waitFor(() => { expect(dataProvider.create).toHaveBeenCalledWith('foo', { data: { bar: 'baz' }, meta: { hello: 'world' }, }); }); await screen.findByText('mutations: 1'); }); it('returns a state typed based on the parametric type', async () => { interface Product extends RaRecord { sku: string; } const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 0, sku: 'abc' } } as any) ), }); let localCreate; let sku; const Dummy = () => { const [create, { data }] = useCreate(); localCreate = create; sku = data && data.sku; return ; }; render( ); expect(sku).toBeUndefined(); localCreate('products', { data: { sku: 'abc' } }); await waitFor(() => { expect(sku).toEqual('abc'); }); }); it('invalidates the getList cache', async () => { const products = [ { id: 1, sku: 'abc' }, { id: 2, sku: 'def' }, ]; const dataProvider = testDataProvider({ getList: () => Promise.resolve({ data: products as any, total: products.length, }), create: () => { const newProduct = { id: 3, sku: 'ghi' }; products.push(newProduct); return Promise.resolve({ data: newProduct as any }); }, }); const ProductPage = () => { const { data: products, isPending } = useGetList('products'); if (isPending) return null; return (
    {products?.map(product => (
  • {product.sku}
  • ))}
); }; const AddProductButton = () => { const [create] = useCreate('products', { data: { sku: 'ghi' } }); return ; }; render( ); const createButton = await screen.findByText('Create'); createButton.click(); await waitFor(() => { expect(screen.queryByText('ghi')).not.toBeNull(); }); }); describe('mutationMode', () => { it('when pessimistic, displays result and success side effects when dataProvider promise resolves', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect(screen.queryByText('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when pessimistic, displays error and error side effects when dataProvider promise rejects', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect(screen.queryByText('something went wrong')).toBeNull(); expect(screen.queryByText('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('something went wrong') ).not.toBeNull(); expect(screen.queryByText('nothing yet')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when optimistic, displays result and success side effects right away', async () => { render(); await screen.findByText('nothing yet'); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when optimistic, displays error and error side effects when dataProvider promise rejects', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('something went wrong') ).not.toBeNull(); expect(screen.queryByText('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); await screen.findByText('nothing yet'); }); it('when undoable, displays result and success side effects right away and fetched on confirm', async () => { render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Confirm').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor( () => { expect(screen.queryByText('mutating')).toBeNull(); }, { timeout: 4000 } ); expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); }); it('when undoable, displays result and success side effects right away and reverts on cancel', async () => { render(); await screen.findByText('nothing yet'); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Cancel').click(); await waitFor(() => { expect(screen.queryByText('Hello World')).toBeNull(); }); expect(screen.queryByText('mutating')).toBeNull(); await screen.findByText('nothing yet'); }); it('when undoable, displays result and success side effects right away and reverts on error', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); await screen.findByText('nothing yet', undefined, { timeout: 5000, }); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Confirm').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await screen.findByText('nothing yet', undefined, { timeout: 4000, }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect(screen.queryByText('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); }); describe('middlewares', () => { it('when pessimistic, it accepts middlewares and displays result and success side effects when dataProvider promise resolves', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect( screen.queryByText('Hello World from middleware') ).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when pessimistic, it accepts middlewares and displays error and error side effects when dataProvider promise rejects', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect(screen.queryByText('something went wrong')).toBeNull(); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('something went wrong') ).not.toBeNull(); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when optimistic, it accepts middlewares and displays result and success side effects right away', async () => { render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect( screen.queryByText('Hello World from middleware') ).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect( screen.queryByText('Hello World from middleware') ).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when optimistic, it accepts middlewares and displays error and error side effects when dataProvider promise rejects', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('something went wrong') ).not.toBeNull(); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); await screen.findByText('nothing yet'); }); it('when undoable, it accepts middlewares and displays result and success side effects right away and fetched on confirm', async () => { render(); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Confirm').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await waitFor( () => { expect(screen.queryByText('mutating')).toBeNull(); }, { timeout: 4000 } ); expect(screen.queryByText('success')).not.toBeNull(); await screen.findByText('Hello World from middleware'); }); it('when undoable, it accepts middlewares and displays result and success side effects right away and reverts on cancel', async () => { render(); await screen.findByText('nothing yet'); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Cancel').click(); await waitFor(() => { expect(screen.queryByText('Hello World')).toBeNull(); }); expect(screen.queryByText('mutating')).toBeNull(); await screen.findByText('nothing yet'); }); it('when undoable, it accepts middlewares and displays result and success side effects right away and reverts on error', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); await screen.findByText('nothing yet', undefined, { timeout: 5000, }); screen.getByText('Create post').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); screen.getByText('Confirm').click(); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).not.toBeNull(); }); await screen.findByText('Hello World', undefined, { timeout: 4000, }); await waitFor( () => { expect(screen.queryByText('success')).toBeNull(); }, { timeout: 5000 } ); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }, 6000); it(`it calls the middlewares in undoable mode even when they got unregistered`, async () => { const middlewareSpy = jest.fn(); render( ); fireEvent.change(screen.getByLabelText('title'), { target: { value: 'Bazinga' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.created'); expect(middlewareSpy).not.toHaveBeenCalled(); fireEvent.click(screen.getByText('Refresh')); expect(screen.queryByText('Bazinga')).toBeNull(); fireEvent.click(screen.getByText('Close')); await waitFor(() => { expect(middlewareSpy).toHaveBeenCalledWith('posts', { data: { id: 2, title: 'Bazinga' }, meta: undefined, }); }); fireEvent.click(screen.getByText('Refresh')); await screen.findByText('Bazinga'); }); it(`it calls the middlewares in optimistic mode even when they got unregistered`, async () => { const middlewareSpy = jest.fn(); render( ); fireEvent.change(screen.getByLabelText('title'), { target: { value: 'Bazinga' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.created'); fireEvent.click(screen.getByText('Close')); expect(middlewareSpy).toHaveBeenCalledWith('posts', { data: { id: 2, title: 'Bazinga' }, meta: undefined, }); fireEvent.click(screen.getByText('Refresh')); await screen.findByText('Bazinga'); }); }); it('invalidates getList query when dataProvider resolves in undoable mode', async () => { render(); fireEvent.change(await screen.findByLabelText('title'), { target: { value: 'New Post' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.created'); fireEvent.click(screen.getByText('Close')); await screen.findByText('3: New Post'); }); });