import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import expect from 'expect';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import {
AuthProvider,
CreateContextProvider,
DataProvider,
Form,
InputProps,
useCreateController,
useInput,
} from '../..';
import { CoreAdminContext } from '../../core';
import { testDataProvider } from '../../dataProvider';
import { useNotificationContext } from '../../notification';
import {
Middleware,
SaveContextProvider,
useRegisterMutationMiddleware,
} from '../saveContext';
import { CreateController } from './CreateController';
import { TestMemoryRouter } from '../../routing';
import {
CanAccess,
DisableAuthentication,
} from './useCreateController.security.stories';
describe('useCreateController', () => {
const defaultProps = {
hasCreate: true,
hasEdit: true,
hasList: true,
hasShow: true,
resource: 'posts',
debounce: 200,
};
it('should call the dataProvider.create() function on save', async () => {
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
),
});
let saveCallback;
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(dataProvider.create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar' },
});
});
it('should execute default success side effects on success', async () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(notificationsSpy).toEqual([
{
message: 'resources.posts.notifications.created',
type: 'info',
notificationOptions: {
messageArgs: {
smart_count: 1,
_: 'ra.notification.created',
},
undoable: false,
},
},
]);
});
it('should execute default failure side effects on failure', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: () => Promise.reject({ message: 'not good' }),
});
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(notificationsSpy).toEqual([
{
message: 'not good',
type: 'error',
notificationOptions: { messageArgs: { _: 'not good' } },
},
]);
});
it('should use the default error message in case no message was provided', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: () => Promise.reject({}),
});
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(notificationsSpy).toEqual([
{
message: 'ra.notification.http_error',
type: 'error',
notificationOptions: { messageArgs: { _: undefined } },
},
]);
});
it('should not trigger a notification in case of a validation error (handled by useNotifyIsFormInvalid)', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: () =>
Promise.reject({ body: { errors: { foo: 'invalid' } } }),
});
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(notificationsSpy).toEqual([]);
});
it('should allow mutationOptions to override the default success side effects', async () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});
const onSuccess = jest.fn();
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(notificationsSpy).toEqual([]);
});
it('should allow the save onSuccess option to override the success side effects override', async () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});
const onSuccess = jest.fn();
const onSuccessSave = jest.fn();
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () =>
saveCallback(
{ foo: 'bar' },
{
onSuccess: onSuccessSave,
}
)
);
expect(onSuccess).not.toHaveBeenCalled();
expect(onSuccessSave).toHaveBeenCalled();
expect(notificationsSpy).toEqual([]);
});
it('should allow mutationOptions to override the default failure side effects', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: () => Promise.reject({ message: 'not good' }),
});
const onError = jest.fn();
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
await waitFor(() => expect(onError).toHaveBeenCalled());
expect(notificationsSpy).toEqual([]);
});
it('should accept meta in mutationOptions', async () => {
let saveCallback;
const create = jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
);
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create,
});
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar' },
meta: { lorem: 'ipsum' },
});
});
it('should accept meta as a save option', async () => {
let saveCallback;
const create = jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
);
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create,
});
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () =>
saveCallback({ foo: 'bar' }, { meta: { lorem: 'ipsum' } })
);
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar' },
meta: { lorem: 'ipsum' },
});
});
it('should allow the save onError option to override the failure side effects override', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create: () => Promise.reject({ message: 'not good' }),
});
const onError = jest.fn();
const onErrorSave = jest.fn();
let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () =>
saveCallback(
{ foo: 'bar' },
{
onError: onErrorSave,
}
)
);
expect(onError).not.toHaveBeenCalled();
expect(onErrorSave).toHaveBeenCalled();
expect(notificationsSpy).toEqual([]);
});
it('should allow transform to transform the data before calling create', async () => {
let saveCallback;
const create = jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
);
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create,
});
const transform = jest.fn().mockImplementationOnce(data => ({
...data,
transformed: true,
}));
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(transform).toHaveBeenCalledWith({ foo: 'bar' });
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar', transformed: true },
});
});
it('should allow the save transform option to override the controller transform option', async () => {
let saveCallback;
const create = jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
);
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
create,
});
const transform = jest.fn();
const transformSave = jest.fn().mockImplementationOnce(data => ({
...data,
transformed: true,
}));
render(
{({ save }) => {
saveCallback = save;
return null;
}}
);
await act(async () =>
saveCallback(
{ foo: 'bar' },
{
transform: transformSave,
}
)
);
expect(transform).not.toHaveBeenCalled();
expect(transformSave).toHaveBeenCalledWith({ foo: 'bar' });
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar', transformed: true },
});
});
it('should allow to register middlewares', async () => {
let saveCallback;
const create = jest
.fn()
.mockImplementationOnce((_, { data }) =>
Promise.resolve({ data: { id: 123, ...data } })
);
const dataProvider = testDataProvider({
create,
});
const middleware: Middleware = jest.fn(
(resource, params, next) => {
return next(resource, {
...params,
meta: { addedByMiddleware: true },
});
}
);
const Child = () => {
useRegisterMutationMiddleware(middleware);
return null;
};
render(
{({
save,
saving,
registerMutationMiddleware,
unregisterMutationMiddleware,
}) => {
saveCallback = save;
return (
);
}}
);
await act(async () => saveCallback({ foo: 'bar' }));
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar' },
meta: { addedByMiddleware: true },
});
expect(middleware).toHaveBeenCalledWith(
'posts',
{
data: { foo: 'bar' },
},
expect.any(Function)
);
});
it('should return errors from the create call', async () => {
const create = jest.fn().mockImplementationOnce(() => {
return Promise.reject({ body: { errors: { foo: 'invalid' } } });
});
const dataProvider = {
create,
} as unknown as DataProvider;
let saveCallback;
render(
{({ save }) => {
saveCallback = save;
return ;
}}
);
await new Promise(resolve => setTimeout(resolve, 10));
let errors;
await act(async () => {
errors = await saveCallback({ foo: 'bar' });
});
expect(errors).toEqual({ foo: 'invalid' });
expect(create).toHaveBeenCalledWith('posts', {
data: { foo: 'bar' },
});
});
it('should allow custom redirect with warnWhenUnsavedChanges', async () => {
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 123 } } as any),
create: (_, { data }) =>
new Promise(resolve =>
setTimeout(
// @ts-ignore
() => resolve({ data: { id: 123, ...data } }),
300
)
),
});
const Input = (props: InputProps) => {
const name = props.source;
const { field } = useInput(props);
return (
<>
>
);
};
const CreateView = () => {
const controllerProps = useCreateController({
...defaultProps,
redirect: 'show',
});
return (
);
};
const ShowView = () => Show
;
render(
} />
} />
);
await screen.findByText('Create');
fireEvent.change(screen.getByLabelText('foo'), {
target: { value: 'bar' },
});
fireEvent.click(screen.getByText('Submit'));
expect(await screen.findByText('Show')).not.toBeNull();
});
describe('Security', () => {
it('should redirect to the /access-denied page when users do not have access', async () => {
render();
await screen.findByText('List');
fireEvent.click(await screen.findByText('posts.create access'));
fireEvent.click(await screen.findByText('Create'));
await screen.findByText('Loading...');
await screen.findByText('Access denied');
});
it('should display the create view when users have access', async () => {
render();
await screen.findByText('List');
fireEvent.click(await screen.findByText('Create'));
await screen.findByText('Loading...');
await screen.findByText('Create view');
});
it('should not call checkAuth nor canAccess when disableAuthentication is true', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render();
await screen.findByText('Create view');
expect(authProvider.checkAuth).not.toHaveBeenCalled();
expect(authProvider.canAccess).not.toHaveBeenCalled();
});
it('should not call checkAuth nor canAccess when disableAuthentication is true even if useAuthState was called before', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render();
await screen.findByText('Create view');
fireEvent.click(await screen.findByText('List'));
await screen.findByText('List view');
fireEvent.click(await screen.findByText('Create'));
await screen.findByText('Create view');
expect(authProvider.checkAuth).toHaveBeenCalledTimes(1);
expect(authProvider.canAccess).not.toHaveBeenCalled();
});
});
});