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();
});