Skip to content

Commit

Permalink
Introduce ra-test package
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Feb 2, 2021
1 parent bdf9413 commit fd77e3f
Show file tree
Hide file tree
Showing 68 changed files with 230 additions and 148 deletions.
4 changes: 4 additions & 0 deletions Makefile
Expand Up @@ -40,6 +40,10 @@ build-ra-core:
@echo "Transpiling ra-core files...";
@cd ./packages/ra-core && yarn -s build

build-ra-test:
@echo "Transpiling ra-test files...";
@cd ./packages/ra-test && yarn -s build

build-ra-ui-materialui:
@echo "Transpiling ra-ui-materialui files...";
@cd ./packages/ra-ui-materialui && yarn -s build
Expand Down
95 changes: 59 additions & 36 deletions docs/UnitTesting.md
Expand Up @@ -17,16 +17,16 @@ That being said, there are still some cases, listed below, where a unit test can

One issue you may run into when attempting to render custom `Create` or `Edit` views is that you need to provide the component with the expected props contained within the react-admin redux store.

Luckily, react-admin provides access to a `TestContext` wrapper component that can be used to initialise your component with many of the expected react-admin props:
Luckily, the `ra-test` package provides access to a `TestContext` wrapper component that can be used to initialise your component with many of the expected react-admin props:

```jsx
import * as React from "react";
import { TestContext } from 'react-admin';
import { mount } from 'enzyme';
import { TestContext } from 'ra-test';
import { render } from '@testing-library/react';
import MyCustomEditView from './my-custom-edit-view';

describe('MyCustomEditView', () => {
let myCustomEditView;
let testUtils;

beforeEach(() => {
const defaultEditProps = {
Expand All @@ -37,7 +37,7 @@ describe('MyCustomEditView', () => {
match: {},
};

myCustomEditView = mount(
testUtils = render(
<TestContext>
<MyCustomEditView {...defaultEditProps} />
</TestContext>
Expand All @@ -57,7 +57,7 @@ At this point, your component should `mount` without errors and you can unit tes
If your component relies on a reducer, you can enable reducers using the `enableReducers` prop:

```jsx
myCustomEditView = mount(
testUtils = render(
<TestContext enableReducers>
<MyCustomEditView />
</TestContext>
Expand All @@ -72,7 +72,7 @@ If you are using `useDispatch` within your components, it is likely you will wan

```jsx
let dispatchSpy;
myCustomEditView = mount(
testUtils = render(
<TestContext>
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
Expand All @@ -82,7 +82,7 @@ myCustomEditView = mount(
);

it('should send the user to another url', () => {
myCustomEditView.find('.next-button').simulate('click');
fireEvent.click(testUtils.getByText('Go to next'));
expect(dispatchSpy).toHaveBeenCalledWith(`/next-url`);
});
```
Expand All @@ -93,61 +93,84 @@ As explained on the [Auth Provider chapter](./Authentication.md#authorization),

In order to avoid regressions and make the design explicit to your co-workers, it's better to unit test which fields are supposed to be displayed or hidden for each permission.

Here is an example with Jest and Enzyme, which is testing the [`UserShow` page of the simple example](https://github.com/marmelab/react-admin/blob/master/examples/simple/src/users/UserShow.js).
Here is an example with Jest and TestingLibrary, which is testing the [`UserShow` page of the simple example](https://github.com/marmelab/react-admin/blob/master/examples/simple/src/users/UserShow.js).

```jsx
// UserShow.spec.js
import * as React from "react";
import { shallow } from 'enzyme';
import { render } from '@testing-library/react';
import { Tab, TextField } from 'react-admin';

import UserShow from './UserShow';

describe('UserShow', () => {
describe('As User', () => {
it('should display one tab', () => {
const wrapper = shallow(<UserShow permissions="user" />);
const testUtils = render(<UserShow permissions="user" />);

const tab = wrapper.find(Tab);
expect(tab.length).toBe(1);
const tabs = testUtils.queryByRole('tab');
expect(tabs.length).toEqual(1);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow permissions="user" />);

const tab = wrapper.find(Tab);
const fields = tab.find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
const dataProvider = {
getOne: jest.fn().resolve({
id: 1,
name: 'Leila'
})
}
const testUtils = render(
<TestContext>
<UserShow permissions="user" id="1" />
</TestContext>
);

expect(testUtils.queryByDisplayValue('1')).not.toBeNull();
expect(testUtils.queryByDisplayValue('Leila')).not.toBeNull();
});
});

describe('As Admin', () => {
it('should display two tabs', () => {
const wrapper = shallow(<UserShow permissions="admin" />);
const testUtils = render(<UserShow permissions="user" />);

const tabs = wrapper.find(Tab);
expect(tabs.length).toBe(2);
const tabs = testUtils.queryByRole('tab');
expect(tabs.length).toEqual(2);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(0).find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
const dataProvider = {
getOne: jest.fn().resolve({
id: 1,
name: 'Leila'
})
}
const testUtils = render(
<TestContext>
<UserShow permissions="user" id="1" />
</TestContext>
);

expect(testUtils.queryByDisplayValue('1')).not.toBeNull();
expect(testUtils.queryByDisplayValue('Leila')).not.toBeNull();
});

it('should show the user role in the second tab', () => {
const wrapper = shallow(<UserShow permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(1).find(TextField);

expect(fields.at(0).prop('source')).toBe('role');
const dataProvider = {
getOne: jest.fn().resolve({
id: 1,
name: 'Leila',
role: 'admin'
})
}
const testUtils = render(
<TestContext>
<UserShow permissions="user" id="1" />
</TestContext>
);

fireEvent.click(testUtils.getByText('Security'));
expect(testUtils.queryByDisplayValue('admin')).not.toBeNull();
});
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/package.json
Expand Up @@ -39,6 +39,7 @@
"final-form": "^4.20.0",
"history": "^4.7.2",
"ignore-styles": "~5.0.1",
"ra-test": "^3.12.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-final-form": "^6.5.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/Authenticated.spec.tsx
Expand Up @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react';

import Authenticated from './Authenticated';
import AuthContext from './AuthContext';
import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { showNotification } from '../actions/notificationActions';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/useAuthState.spec.tsx
Expand Up @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react';

import useAuthState from './useAuthState';
import AuthContext from './AuthContext';
import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';

const UseAuth = ({ children, authParams }: any) => {
const res = useAuthState(authParams);
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/useAuthenticated.spec.tsx
Expand Up @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react';

import Authenticated from './Authenticated';
import AuthContext from './AuthContext';
import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { showNotification } from '../actions/notificationActions';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/usePermissions.spec.tsx
Expand Up @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react';

import usePermissions from './usePermissions';
import AuthContext from './AuthContext';
import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';

const UsePermissions = ({ children, authParams }: any) => {
const res = usePermissions(authParams);
Expand Down
Expand Up @@ -6,7 +6,7 @@ import { CreateBase } from './CreateBase';
import { useSaveContext } from './SaveContext';
import { DataProviderContext } from '../../dataProvider';
import { DataProvider } from '../../types';
import { renderWithRedux } from '../../util';
import { renderWithRedux } from 'ra-test';

describe('CreateBase', () => {
afterEach(cleanup);
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/details/EditBase.spec.tsx
Expand Up @@ -6,7 +6,7 @@ import { EditBase } from './EditBase';
import { useSaveContext } from './SaveContext';
import { DataProviderContext } from '../../dataProvider';
import { DataProvider } from '../../types';
import { renderWithRedux } from '../../util';
import { renderWithRedux } from 'ra-test';

describe('EditBase', () => {
afterEach(cleanup);
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { act } from '@testing-library/react';

import { getRecord } from './useCreateController';
import { CreateController } from './CreateController';
import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { DataProviderContext } from '../../dataProvider';
import { DataProvider } from '../../types';

Expand Down
Expand Up @@ -3,7 +3,7 @@ import expect from 'expect';
import { act, waitFor } from '@testing-library/react';

import { EditController } from './EditController';
import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { DataProviderContext } from '../../dataProvider';
import { DataProvider } from '../../types';
import { SaveContextProvider } from '../../../esm';
Expand Down
Expand Up @@ -3,7 +3,7 @@ import expect from 'expect';

import ReferenceArrayFieldController from './ReferenceArrayFieldController';
import { DataProviderContext } from '../../dataProvider';
import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';

describe('<ReferenceArrayFieldController />', () => {
it('should set the loaded prop to false when related records are not yet fetched', () => {
Expand Down
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import expect from 'expect';

import ReferenceFieldController from './ReferenceFieldController';
import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { DataProviderContext } from '../../dataProvider';

const defaultState = {
Expand Down
Expand Up @@ -3,7 +3,7 @@ import { waitFor } from '@testing-library/react';
import expect from 'expect';

import ReferenceManyFieldController from './ReferenceManyFieldController';
import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';

describe('<ReferenceManyFieldController />', () => {
it('should set loaded to false when related records are not yet fetched', async () => {
Expand Down
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import expect from 'expect';
import { waitFor, fireEvent } from '@testing-library/react';
import ReferenceArrayInputController from './ReferenceArrayInputController';
import { renderWithRedux } from '../../util';
import { renderWithRedux } from 'ra-test';
import { CRUD_GET_MATCHING, CRUD_GET_MANY } from '../../../lib';

describe('<ReferenceArrayInputController />', () => {
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { fireEvent, waitFor } from '@testing-library/react';
import omit from 'lodash/omit';
import expect from 'expect';

import renderWithRedux from '../../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import ReferenceInputController from './ReferenceInputController';
import { DataProviderContext } from '../../dataProvider';

Expand Down
@@ -1,4 +1,4 @@
import renderHook from '../../util/renderHook';
import { renderHook } from 'ra-test';
import useMatchingReferences from './useGetMatchingReferences';

describe('useMatchingReferences', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useFilterState.spec.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
import renderHook from '../util/renderHook';
import { renderHook } from 'ra-test';
import useFilterState from './useFilterState';
import { render, act } from '@testing-library/react';

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useListController.spec.tsx
Expand Up @@ -10,7 +10,7 @@ import {
sanitizeListRestProps,
} from './useListController';

import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import { CRUD_CHANGE_LIST_PARAMS } from '../actions';
import { SORT_ASC } from '../reducer/admin/resource/list/queryReducer';

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useListParams.spec.tsx
Expand Up @@ -5,7 +5,7 @@ import { Router } from 'react-router-dom';
import { stringify } from 'query-string';
import { createMemoryHistory } from 'history';
import { fireEvent, waitFor } from '@testing-library/react';
import { renderWithRedux, TestContext } from '../util';
import { renderWithRedux, TestContext } from 'ra-test';

import useListParams, { getQuery, getNumberOrDefault } from './useListParams';
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/usePaginationState.spec.ts
@@ -1,4 +1,4 @@
import renderHook from '../util/renderHook';
import { renderHook } from 'ra-test';
import usePaginationState from './usePaginationState';
import { act } from '@testing-library/react';

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useReference.spec.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import expect from 'expect';

import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import useReference from './useReference';
import { DataProviderContext } from '../dataProvider';

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/controller/useSortState.spec.ts
@@ -1,4 +1,4 @@
import renderHook from '../util/renderHook';
import { renderHook } from 'ra-test';
import useSortState, { defaultSort } from './useSortState';
import { act } from '@testing-library/react';

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/core/CoreAdminRouter.spec.tsx
Expand Up @@ -4,7 +4,7 @@ import expect from 'expect';
import { Router, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import CoreAdminRouter from './CoreAdminRouter';
import AuthContext from '../auth/AuthContext';
import Resource from './Resource';
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/core/Resource.spec.tsx
Expand Up @@ -6,7 +6,7 @@ import { createMemoryHistory } from 'history';

import Resource from './Resource';
import { registerResource, unregisterResource } from '../actions';
import renderWithRedux from '../util/renderWithRedux';
import { renderWithRedux } from 'ra-test';
import AuthContext from '../auth/AuthContext';

const PostList = () => <div>PostList</div>;
Expand Down
3 changes: 1 addition & 2 deletions packages/ra-core/src/dataProvider/Mutation.spec.tsx
Expand Up @@ -3,10 +3,9 @@ import { fireEvent, waitFor, act, render } from '@testing-library/react';
import expect from 'expect';

import Mutation from './Mutation';
import renderWithRedux from '../util/renderWithRedux';
import { showNotification, refreshView, setListSelectedIds } from '../actions';
import DataProviderContext from './DataProviderContext';
import TestContext from '../util/TestContext';
import { renderWithRedux, TestContext } from 'ra-test';
import { useNotify } from '../sideEffect';
import { History } from 'history';

Expand Down
3 changes: 1 addition & 2 deletions packages/ra-core/src/dataProvider/Query.spec.tsx
Expand Up @@ -4,8 +4,7 @@ import expect from 'expect';

import Query from './Query';
import { CoreAdmin, Resource } from '../core';
import renderWithRedux from '../util/renderWithRedux';
import TestContext from '../util/TestContext';
import { renderWithRedux, TestContext } from 'ra-test';
import DataProviderContext from './DataProviderContext';
import { showNotification, refreshView, setListSelectedIds } from '../actions';
import { useNotify, useRefresh } from '../sideEffect';
Expand Down

0 comments on commit fd77e3f

Please sign in to comment.