import * as React from 'react'; import { act, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import expect from 'expect'; import { QueryClient, useMutationState } from '@tanstack/react-query'; import { CoreAdminContext } from '../core'; import { RaRecord } from '../types'; import { useUpdate } from './useUpdate'; import { ErrorCase as ErrorCasePessimistic, SuccessCase as SuccessCasePessimistic, WithMiddlewaresSuccess as WithMiddlewaresSuccessPessimistic, WithMiddlewaresError as WithMiddlewaresErrorPessimistic, } from './useUpdate.pessimistic.stories'; import { ErrorCase as ErrorCaseOptimistic, SuccessCase as SuccessCaseOptimistic, WithMiddlewaresSuccess as WithMiddlewaresSuccessOptimistic, WithMiddlewaresError as WithMiddlewaresErrorOptimistic, UndefinedValues as UndefinedValuesOptimistic, } from './useUpdate.optimistic.stories'; import { ErrorCase as ErrorCaseUndoable, SuccessCase as SuccessCaseUndoable, WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable, WithMiddlewaresError as WithMiddlewaresErrorUndoable, } from './useUpdate.undoable.stories'; import { Middleware, MutationMode, Params, InvalidateList, } from './useUpdate.stories'; describe('useUpdate', () => { describe('mutate', () => { it('returns a callback that can be used with update arguments', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); }); it('returns a callback that can be used with no arguments', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); localUpdate = update; return ; }; render( ); localUpdate(); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); }); it('uses a custom mutationFn with mutation middlewares', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; const customMutationFn = jest.fn(async params => ({ id: params.id, title: params.data?.title, middlewareApplied: params.data?.middlewareApplied, })); let localUpdate; let mutationData; const Dummy = () => { const [update, { data }] = useUpdate(undefined, undefined, { mutationFn: customMutationFn, getMutateWithMiddlewares: mutate => async (resource, params) => mutate(resource, { ...params, data: { ...params.data, middlewareApplied: true, }, }), }); localUpdate = update; mutationData = data; return ; }; render( ); localUpdate('foo', { id: 1, data: { title: 'Hello' }, previousData: { id: 1, title: 'World' }, }); await waitFor(() => { expect(customMutationFn).toHaveBeenCalledWith({ resource: 'foo', id: 1, data: { title: 'Hello', middlewareApplied: true, }, previousData: { id: 1, title: 'World' }, }); }); expect(dataProvider.update).not.toHaveBeenCalled(); await waitFor(() => { expect(mutationData).toEqual({ id: 1, title: 'Hello', middlewareApplied: true, }); }); }); it('uses the latest declaration time mutationMode', async () => { // This story uses the pessimistic mode by default render(); fireEvent.click( screen.getByText('Change mutation mode to optimistic') ); fireEvent.click(screen.getByText('Update title')); // 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('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).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('Update title')); // 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('accepts falsy value that are not null nor undefined as the record id', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate('foo', { id: 0, data: { bar: 'baz' }, previousData: { id: 0, bar: 'bar' }, }); localUpdate = update; return ; }; render( ); localUpdate(); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 0, data: { bar: 'baz' }, previousData: { id: 0, bar: 'bar' }, }); }); }); it('replaces hook call time params by and callback time params', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); localUpdate = update; return ; }; render( ); localUpdate(undefined, { data: { foo: 456 } }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { foo: 456 }, previousData: { id: 1, bar: 'bar' }, }); }); }); it('accepts a meta parameter', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { hello: 'world' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { hello: 'world' }, }); }); }); }); it('sets the mutationKey', async () => { const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate('foo'); localUpdate = update; return ; }; const Observe = () => { const mutation = useMutationState({ filters: { mutationKey: ['foo', 'update'], }, }); return mutations: {mutation.length}; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { hello: 'world' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { hello: 'world' }, }); }); await screen.findByText('mutations: 1'); }); describe('data', () => { it('returns a data typed based on the parametric type', async () => { interface Product extends RaRecord { sku: string; } const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, sku: 'abc' } } as any) ), } as any; let localUpdate; let sku; const Dummy = () => { const [update, { data }] = useUpdate(); localUpdate = update; sku = data && data.sku; return ; }; render( ); expect(sku).toBeUndefined(); localUpdate('products', { id: 1, data: { sku: 'abc' }, previousData: { id: 1, sku: 'bcd' }, }); await waitFor(() => { expect(sku).toEqual('abc'); }); }); }); describe('mutationMode', () => { it('when pessimistic, displays result and success side effects when dataProvider promise resolves', async () => { render(); screen.getByText('Update title').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('Update title').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('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it('when optimistic, displays result and success side effects right away', async () => { render(); await screen.findByText('Hello'); screen.getByText('Update title').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(); await screen.findByText('Hello'); screen.getByText('Update title').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('Hello'); }); it('when optimistic, does not erase values if the payload contains undefined values', async () => { render(); await screen.findByText('{"id":1,"title":"Hello"}'); screen.getByText('Update title').click(); await screen.findByText('{"id":1,"title":"world"}'); // and not just {"title":"world"} }); it('when undoable, displays result and success side effects right away and fetched on confirm', async () => { render(); await screen.findByText('Hello'); act(() => { screen.getByText('Update title').click(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); act(() => { 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('Hello'); act(() => { screen.getByText('Update title').click(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); act(() => { screen.getByText('Cancel').click(); }); await waitFor(() => { expect(screen.queryByText('Hello World')).toBeNull(); }); expect(screen.queryByText('mutating')).toBeNull(); await screen.findByText('Hello'); }); 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('Hello'); screen.getByText('Update title').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', undefined, { timeout: 4000 }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect(screen.queryByText('Hello World')).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); }); describe('query cache', () => { it('updates getList query cache when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData(['foo', 'getList'], { data: [{ id: 1, bar: 'bar' }], total: 1, }); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect(queryClient.getQueryData(['foo', 'getList'])).toEqual({ data: [{ id: 1, bar: 'baz' }], total: 1, }); }); }); it('updates getList query cache with pageInfo when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData(['foo', 'getList'], { data: [{ id: 1, bar: 'bar' }], pageInfo: { hasPreviousPage: false, hasNextPage: true, }, }); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect(queryClient.getQueryData(['foo', 'getList'])).toEqual({ data: [{ id: 1, bar: 'baz' }], pageInfo: { hasPreviousPage: false, hasNextPage: true, }, }); }); }); it('updates getManyReference query cache with pageInfo when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData(['foo', 'getManyReference'], { data: [{ id: 1, bar: 'bar' }], pageInfo: { hasPreviousPage: false, hasNextPage: true, }, }); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect( queryClient.getQueryData(['foo', 'getManyReference']) ).toEqual({ data: [{ id: 1, bar: 'baz' }], pageInfo: { hasPreviousPage: false, hasNextPage: true, }, }); }); }); it('updates getInfiniteList query cache when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData(['foo', 'getInfiniteList'], { pages: [{ data: [{ id: 1, bar: 'bar' }], total: 1 }], pageParams: [], }); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect( queryClient.getQueryData(['foo', 'getInfiniteList']) ).toEqual({ pages: [{ data: [{ id: 1, bar: 'baz' }], total: 1 }], pageParams: [], }); }); }); it('invalidates getList query when dataProvider resolves in undoable mode', async () => { render(); fireEvent.change(await screen.findByDisplayValue('Hello'), { target: { value: 'Hello changed' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.updated'); fireEvent.click(screen.getByText('Close')); await screen.findByText(/Hello changed/); }); describe('pessimistic mutation mode', () => { it('updates getOne query cache when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: undefined }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: undefined }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); }); it('updates getOne query cache when dataProvider promise resolves with meta', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate(); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); }); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); }); it('updates getOne query cache when dataProvider promise resolves with meta at hook time', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; let localUpdate; const Dummy = () => { const [update] = useUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); localUpdate = update; return ; }; render( ); localUpdate(); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); }); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); }); }); describe('optimistic mutation mode', () => { it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: undefined }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; const queryClientSpy = jest.spyOn( queryClient, 'invalidateQueries' ); let localUpdate; const Dummy = () => { const [update] = useUpdate(undefined, undefined, { mutationMode: 'optimistic', }); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: undefined }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, }); }); await waitFor(() => { expect(queryClientSpy).toHaveBeenCalledWith({ queryKey: [ 'foo', 'getOne', { id: '1', meta: undefined }, ], }); }); }); it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves with meta', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; const queryClientSpy = jest.spyOn( queryClient, 'invalidateQueries' ); let localUpdate; const Dummy = () => { const [update] = useUpdate(undefined, undefined, { mutationMode: 'optimistic', }); localUpdate = update; return ; }; render( ); localUpdate('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); }); await waitFor(() => { expect(queryClientSpy).toHaveBeenCalledWith({ queryKey: [ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ], }); }); }); it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves with meta at hook time', async () => { const queryClient = new QueryClient(); queryClient.setQueryData( ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], { id: 1, bar: 'bar' } ); const dataProvider = { update: jest.fn(() => Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) ), } as any; const queryClientSpy = jest.spyOn( queryClient, 'invalidateQueries' ); let localUpdate; const Dummy = () => { const [update] = useUpdate( 'foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }, { mutationMode: 'optimistic', } ); localUpdate = update; return ; }; render( ); localUpdate(); await waitFor(() => { expect( queryClient.getQueryData([ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ]) ).toEqual({ id: 1, bar: 'baz', }); }); await waitFor(() => { expect(dataProvider.update).toHaveBeenCalledWith('foo', { id: 1, data: { bar: 'baz' }, previousData: { id: 1, bar: 'bar' }, meta: { key: 'value' }, }); }); await waitFor(() => { expect(queryClientSpy).toHaveBeenCalledWith({ queryKey: [ 'foo', 'getOne', { id: '1', meta: { key: 'value' } }, ], }); }); }); }); }); describe('middlewares', () => { it('when pessimistic, it accepts middlewares and displays result and success side effects when dataProvider promise resolves', async () => { render(); await screen.findByText('Hello'); screen.getByText('Update title').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(); await screen.findByText('Hello'); screen.getByText('Update title').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(); await screen.findByText('Hello'); screen.getByText('Update title').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(); await screen.findByText('Hello'); screen.getByText('Update title').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('Hello'); }); it('when undoable, it accepts middlewares and displays result and success side effects right away and fetched on confirm', async () => { render(); await screen.findByText('Hello'); act(() => { screen.getByText('Update title').click(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); act(() => { 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 from middleware') ).not.toBeNull(); }); it('when undoable, it accepts middlewares and displays result and success side effects right away and reverts on cancel', async () => { render(); await screen.findByText('Hello'); act(() => { screen.getByText('Update title').click(); }); await waitFor(() => { expect(screen.queryByText('success')).not.toBeNull(); expect(screen.queryByText('Hello World')).not.toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); act(() => { screen.getByText('Cancel').click(); }); await waitFor(() => { expect(screen.queryByText('Hello World')).toBeNull(); }); expect(screen.queryByText('mutating')).toBeNull(); await screen.findByText('Hello'); }); 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('Hello'); screen.getByText('Update title').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', undefined, { timeout: 4000 }); await waitFor(() => { expect(screen.queryByText('success')).toBeNull(); expect( screen.queryByText('Hello World from middleware') ).toBeNull(); expect(screen.queryByText('mutating')).toBeNull(); }); }); it(`it calls the middlewares in undoable mode even when they got unregistered`, async () => { const middlewareSpy = jest.fn(); render( ); fireEvent.click(await screen.findByText('Hello')); fireEvent.change(await screen.findByLabelText('title'), { target: { value: 'Bazinga' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.updated'); expect(middlewareSpy).not.toHaveBeenCalled(); fireEvent.click(screen.getByText('Close')); await waitFor(() => { expect(middlewareSpy).toHaveBeenCalledWith('posts', { id: '1', data: { author: 'John Doe', id: 1, title: 'Bazinga' }, meta: undefined, previousData: { author: 'John Doe', id: 1, title: 'Bazinga', }, }); }); }); it(`it calls the middlewares in optimistic mode even when they got unregistered`, async () => { const middlewareSpy = jest.fn(); render( ); fireEvent.click(await screen.findByText('Hello')); fireEvent.change(await screen.findByLabelText('title'), { target: { value: 'Bazinga' }, }); fireEvent.click(screen.getByText('Save')); await screen.findByText('resources.posts.notifications.updated'); fireEvent.click(screen.getByText('Close')); await waitFor(() => { expect(middlewareSpy).toHaveBeenCalledWith('posts', { id: '1', data: { author: 'John Doe', id: 1, title: 'Bazinga' }, meta: undefined, previousData: { author: 'John Doe', id: 1, title: 'Bazinga', }, }); }); }); }); }); afterEach(() => { jest.restoreAllMocks(); });