diff --git a/packages/ra-core/src/controller/ListController.spec.tsx b/packages/ra-core/src/controller/ListController.spec.tsx
index 85b2e4c568d..b3b962abc25 100644
--- a/packages/ra-core/src/controller/ListController.spec.tsx
+++ b/packages/ra-core/src/controller/ListController.spec.tsx
@@ -9,7 +9,7 @@ import ListController, {
sanitizeListRestProps,
} from './ListController';
-import TestContext from '../util/TestContext';
+import renderWithRedux from '../util/renderWithRedux';
import { CRUD_CHANGE_LIST_PARAMS } from '../actions';
describe('ListController', () => {
@@ -60,24 +60,13 @@ describe('ListController', () => {
children: fakeComponent,
};
- let reduxStore;
- let dispatch;
-
- const { getByLabelText } = render(
-
- {({ store }) => {
- reduxStore = store;
- dispatch = jest.spyOn(store, 'dispatch');
- return ;
- }}
-
+ const { getByLabelText, dispatch, reduxStore } = renderWithRedux(
+ ,
+ {
+ admin: {
+ resources: { posts: { list: { params: {} } } },
+ },
+ }
);
const searchInput = getByLabelText('search');
@@ -110,22 +99,13 @@ describe('ListController', () => {
children: fakeComponent,
};
- let dispatch;
-
- const { getByLabelText } = render(
-
- {({ store }) => {
- dispatch = jest.spyOn(store, 'dispatch');
- return ;
- }}
-
+ const { getByLabelText, dispatch } = renderWithRedux(
+ ,
+ {
+ admin: {
+ resources: { posts: { list: { params: {} } } },
+ },
+ }
);
const searchInput = getByLabelText('search');
@@ -148,30 +128,19 @@ describe('ListController', () => {
children: fakeComponent,
};
- let reduxStore;
- let dispatch;
-
- const { getByLabelText } = render(
- ,
+ {
+ admin: {
+ resources: {
+ posts: {
+ list: {
+ params: { filter: { q: 'hello' } },
},
},
},
- }}
- enableReducers
- >
- {({ store }) => {
- reduxStore = store;
- dispatch = jest.spyOn(store, 'dispatch');
- return ;
- }}
-
+ },
+ }
);
const searchInput = getByLabelText('search');
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx
index 4a070970386..7a3acc8caad 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx
@@ -1,192 +1,266 @@
import React from 'react';
-import assert from 'assert';
-import { shallow } from 'enzyme';
+import { render, cleanup } from 'react-testing-library';
-import { UnconnectedReferenceFieldController as ReferenceFieldController } from './ReferenceFieldController';
+import ReferenceFieldController from './ReferenceFieldController';
+import renderWithRedux from '../../util/renderWithRedux';
+import { crudGetManyAccumulate } from '../../actions';
+
+const defaultState = {
+ admin: {
+ resources: { posts: { data: { 123: { id: 123, title: 'foo' } } } },
+ },
+};
describe('', () => {
+ afterEach(cleanup);
it('should call crudGetManyAccumulate on componentDidMount if reference source is defined', () => {
- const crudGetManyAccumulate = jest.fn();
- shallow(
+ const { dispatch } = renderWithRedux(
children)} // eslint-disable-line react/no-children-prop
record={{ id: 1, postId: 123 }}
source="postId"
- referenceRecord={{ id: 123, title: 'foo' }}
reference="posts"
basePath=""
- crudGetManyAccumulate={crudGetManyAccumulate}
- />
+ />,
+ defaultState
);
- assert.equal(crudGetManyAccumulate.mock.calls.length, 1);
+ expect(dispatch).toBeCalledTimes(1);
+ expect(dispatch).toBeCalledWith(crudGetManyAccumulate('posts', [123]));
});
+
it('should not call crudGetManyAccumulate on componentDidMount if reference source is null or undefined', () => {
- const crudGetManyAccumulate = jest.fn();
- shallow(
+ const { dispatch } = renderWithRedux(
children)} // eslint-disable-line react/no-children-prop
record={{ id: 1, postId: null }}
source="postId"
- referenceRecord={{ id: 123, title: 'foo' }}
reference="posts"
basePath=""
- crudGetManyAccumulate={crudGetManyAccumulate}
- />
+ />,
+ defaultState
);
- assert.equal(crudGetManyAccumulate.mock.calls.length, 0);
+ expect(dispatch).toBeCalledTimes(0);
});
- it('should render a link to the Edit page of the related record by default', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+
+ it('should pass resourceLinkPath and referenceRecord to its children', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
+ ,
+ defaultState
);
- assert.equal(children.mock.calls[0][0].resourceLinkPath, '/posts/123');
+
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/posts/123',
+ });
});
- it('should render a link to the Edit page of the related record when the resource contains slashes', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+
+ it('should accept slashes in resource name', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
- );
- assert.equal(
- children.mock.calls[0][0].resourceLinkPath,
- '/prefix/posts/123'
+ ,
+ {
+ admin: {
+ resources: {
+ 'prefix/posts': {
+ data: { 123: { id: 123, title: 'foo' } },
+ },
+ },
+ },
+ }
);
+
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/prefix/posts/123',
+ });
});
- it('should render a link to the Edit page of the related record when the resource is named edit or show', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+
+ it('should accept edit as resource name', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
+ ,
+ {
+ admin: {
+ resources: {
+ edit: {
+ data: { 123: { id: 123, title: 'foo' } },
+ },
+ },
+ },
+ }
);
- assert.equal(children.mock.calls[0][0].resourceLinkPath, '/edit/123');
- shallow(
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/edit/123',
+ });
+ });
+
+ it('should accept show as resource name', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
+ ,
+ {
+ admin: {
+ resources: {
+ show: {
+ data: { 123: { id: 123, title: 'foo' } },
+ },
+ },
+ },
+ }
);
- assert.equal(children.mock.calls[1][0].resourceLinkPath, '/show/123');
+
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/show/123',
+ });
});
+
it('should render a link to the Show page of the related record when the linkType is show', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
- );
- assert.equal(
- children.mock.calls[0][0].resourceLinkPath,
- '/posts/123/show'
+ ,
+ defaultState
);
+
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/posts/123/show',
+ });
});
- it('should render a link to the Show page of the related record when the resource is named edit or show and linkType is show', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+
+ it('should accept edit as resource name when linkType is show', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
- );
- assert.equal(
- children.mock.calls[0][0].resourceLinkPath,
- '/edit/123/show'
+ ,
+ {
+ admin: {
+ resources: {
+ edit: {
+ data: { 123: { id: 123, title: 'foo' } },
+ },
+ },
+ },
+ }
);
- shallow(
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/edit/123/show',
+ });
+ });
+
+ it('should accept show as resource name when linkType is show', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
+ ,
+ {
+ admin: {
+ resources: {
+ show: {
+ data: { 123: { id: 123, title: 'foo' } },
+ },
+ },
+ },
+ }
);
- assert.equal(
- children.mock.calls[1][0].resourceLinkPath,
- '/show/123/show'
- );
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: '/show/123/show',
+ });
});
- it('should render no link when the linkType is false', () => {
- const children = jest.fn();
- const crudGetManyAccumulate = jest.fn();
- shallow(
+
+ it('should set resourceLinkPath to false when the linkType is false', () => {
+ const children = jest.fn().mockReturnValue(children);
+ renderWithRedux(
{children}
-
+ ,
+ defaultState
);
- assert.equal(children.mock.calls[0][0].resourceLinkPath, false);
+
+ expect(children).toBeCalledWith({
+ isLoading: false,
+ referenceRecord: { id: 123, title: 'foo' },
+ resourceLinkPath: false,
+ });
});
});
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx
index 06bc814e5dc..be27eeb55b9 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx
@@ -1,25 +1,14 @@
-import { Component, ReactNode } from 'react';
-import { connect } from 'react-redux';
-import get from 'lodash/get';
+import { FunctionComponent, ReactNode, ReactElement } from 'react';
+import { Record } from '../../types';
+import useReference, { UseReferenceProps } from './useReference';
-import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions';
-import { linkToRecord } from '../../util';
-import { Record, Dispatch } from '../../types';
-
-interface ChildrenFuncParams {
- isLoading: boolean;
- referenceRecord: Record;
- resourceLinkPath: string | boolean;
-}
interface Props {
allowEmpty?: boolean;
basePath: string;
- children: (params: ChildrenFuncParams) => ReactNode;
- crudGetManyAccumulate: Dispatch;
+ children: (params: UseReferenceProps) => ReactNode;
record?: Record;
reference: string;
- referenceRecord?: Record;
resource: string;
source: string;
linkType: string | boolean;
@@ -54,69 +43,11 @@ interface Props {
*
*
*/
-export class UnconnectedReferenceFieldController extends Component {
- public static defaultProps: Partial = {
- allowEmpty: false,
- linkType: 'edit',
- referenceRecord: null,
- record: { id: '' },
- };
-
- componentDidMount() {
- this.fetchReference(this.props);
- }
-
- componentWillReceiveProps(nextProps) {
- if (this.props.record.id !== nextProps.record.id) {
- this.fetchReference(nextProps);
- }
- }
-
- fetchReference(props) {
- const source = get(props.record, props.source);
- if (source !== null && typeof source !== 'undefined') {
- this.props.crudGetManyAccumulate(props.reference, [source]);
- }
- }
-
- render() {
- const {
- allowEmpty,
- basePath,
- children,
- linkType,
- record,
- reference,
- referenceRecord,
- resource,
- source,
- } = this.props;
- const rootPath = basePath.replace(resource, reference);
- const resourceLinkPath = !linkType
- ? false
- : linkToRecord(rootPath, get(record, source), linkType as string);
-
- return children({
- isLoading: !referenceRecord && !allowEmpty,
- referenceRecord,
- resourceLinkPath,
- });
- }
-}
-
-const mapStateToProps = (state, props) => ({
- referenceRecord:
- state.admin.resources[props.reference] &&
- state.admin.resources[props.reference].data[
- get(props.record, props.source)
- ],
-});
-
-const ReferenceFieldController = connect(
- mapStateToProps,
- {
- crudGetManyAccumulate: crudGetManyAccumulateAction,
- }
-)(UnconnectedReferenceFieldController);
+export const ReferenceFieldController: FunctionComponent = ({
+ children,
+ ...props
+}) => {
+ return children(useReference(props)) as ReactElement;
+};
export default ReferenceFieldController;
diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts
index fc57420b5d3..9022a0d35e0 100644
--- a/packages/ra-core/src/controller/field/index.ts
+++ b/packages/ra-core/src/controller/field/index.ts
@@ -1,9 +1,11 @@
import ReferenceArrayFieldController from './ReferenceArrayFieldController';
import ReferenceFieldController from './ReferenceFieldController';
import ReferenceManyFieldController from './ReferenceManyFieldController';
+import useReference from './useReference';
export {
ReferenceArrayFieldController,
ReferenceFieldController,
+ useReference,
ReferenceManyFieldController,
};
diff --git a/packages/ra-core/src/controller/field/useReference.ts b/packages/ra-core/src/controller/field/useReference.ts
new file mode 100644
index 00000000000..b2f8415fa1e
--- /dev/null
+++ b/packages/ra-core/src/controller/field/useReference.ts
@@ -0,0 +1,101 @@
+import { useEffect } from 'react';
+// @ts-ignore
+import { useDispatch, useSelector } from 'react-redux';
+import get from 'lodash/get';
+
+import { crudGetManyAccumulate } from '../../actions';
+import { linkToRecord } from '../../util';
+import { Record, ReduxState } from '../../types';
+
+interface Option {
+ allowEmpty?: boolean;
+ basePath: string;
+ record?: Record;
+ reference: string;
+ resource: string;
+ source: string;
+ linkType: string | boolean;
+}
+
+export interface UseReferenceProps {
+ isLoading: boolean;
+ referenceRecord: Record;
+ resourceLinkPath: string | false;
+}
+
+/**
+ * @typedef ReferenceProps
+ * @type {Object}
+ * @property {boolean} isLoading: boolean indicating if the reference has loaded
+ * @property {Object} referenceRecord: the referenced record.
+ * @property {string | false} resourceLinkPath link to the page of the related record (depends on linkType) (false is no link)
+ */
+
+/**
+ * Fetch reference record, and return it when avaliable
+ *
+ * The reference prop sould be the name of one of the components
+ * added as child.
+ *
+ * @example
+ *
+ * const { isLoading, referenceRecord, resourceLinkPath } = useReference({
+ * source: 'userId',
+ * reference: 'users',
+ * record: {
+ * userId: 7
+ * }
+ * });
+ *
+ * @param {Object} option
+ * @param {boolean} option.allowEmpty do we allow for no referenced record (default to false)
+ * @param {string} option.basePath basepath to current resource
+ * @param {string | false} option.linkType The type of the link toward the referenced record. edit, show of false for no link (default to edit)
+ * @param {Object} option.record The The current resource record
+ * @param {string} option.reference The linked resource name
+ * @param {string} option.resource The current resource name
+ * @param {string} option.source The key of the linked resource identifier
+ *
+ * @returns {ReferenceProps} The reference props
+ */
+export const useReference = ({
+ allowEmpty = false,
+ basePath,
+ linkType = 'edit',
+ record = { id: '' },
+ reference,
+ resource,
+ source,
+}: Option): UseReferenceProps => {
+ const sourceId = get(record, source);
+ const referenceRecord = useSelector(
+ getReferenceRecord(sourceId, reference)
+ );
+ const dispatch = useDispatch();
+ useEffect(fetchReference(sourceId, reference, dispatch), [
+ sourceId,
+ reference,
+ ]);
+ const rootPath = basePath.replace(resource, reference);
+ const resourceLinkPath = !linkType
+ ? false
+ : linkToRecord(rootPath, sourceId, linkType as string);
+
+ return {
+ isLoading: !referenceRecord && !allowEmpty,
+ referenceRecord,
+ resourceLinkPath,
+ };
+};
+
+const getReferenceRecord = (sourceId, reference) => (state: ReduxState) =>
+ state.admin.resources[reference] &&
+ state.admin.resources[reference].data[sourceId];
+
+const fetchReference = (sourceId, reference, dispatch) => () => {
+ if (sourceId !== null && typeof sourceId !== 'undefined') {
+ dispatch(crudGetManyAccumulate(reference, [sourceId]));
+ }
+};
+
+export default useReference;
diff --git a/packages/ra-core/src/fetch/Mutation.spec.tsx b/packages/ra-core/src/fetch/Mutation.spec.tsx
index e4334b49e83..c3cf08e400b 100644
--- a/packages/ra-core/src/fetch/Mutation.spec.tsx
+++ b/packages/ra-core/src/fetch/Mutation.spec.tsx
@@ -10,62 +10,42 @@ import expect from 'expect';
import Mutation from './Mutation';
import CoreAdmin from '../CoreAdmin';
import Resource from '../Resource';
-import TestContext from '../util/TestContext';
+import renderWithRedux from '../util/renderWithRedux';
describe('Mutation', () => {
afterEach(cleanup);
it('should render its child', () => {
- const { getByTestId } = render(
-
- {() => (
-
- {() => Hello
}
-
- )}
-
+ const { getByTestId } = renderWithRedux(
+
+ {() => Hello
}
+
);
expect(getByTestId('test').textContent).toBe('Hello');
});
it('should pass a callback to trigger the mutation', () => {
let callback = null;
- render(
-
- {() => (
-
- {mutate => {
- callback = mutate;
- return Hello
;
- }}
-
- )}
-
+ renderWithRedux(
+
+ {mutate => {
+ callback = mutate;
+ return Hello
;
+ }}
+
);
expect(callback).toBeInstanceOf(Function);
});
it('should dispatch a fetch action when the mutation callback is triggered', () => {
- let dispatchSpy;
const myPayload = {};
- const { getByText } = render(
-
- {({ store }) => {
- dispatchSpy = jest.spyOn(store, 'dispatch');
- return (
-
- {mutate => }
-
- );
- }}
-
+ const { getByText, reduxStore, dispatch } = renderWithRedux(
+
+ {mutate => }
+
);
fireEvent.click(getByText('Hello'));
- const action = dispatchSpy.mock.calls[0][0];
+ const action = dispatch.mock.calls[0][0];
expect(action.type).toEqual('CUSTOM_FETCH');
expect(action.payload).toEqual(myPayload);
expect(action.meta.fetch).toEqual('mytype');
@@ -74,25 +54,17 @@ describe('Mutation', () => {
it('should update the loading state when the mutation callback is triggered', () => {
const myPayload = {};
- const { getByText } = render(
-
- {() => (
-
+ {(mutate, { loading }) => (
+
+ Hello
+
)}
-
+
);
expect(getByText('Hello').className).toEqual('idle');
fireEvent.click(getByText('Hello'));
diff --git a/packages/ra-core/src/fetch/Query.spec.tsx b/packages/ra-core/src/fetch/Query.spec.tsx
index 4316c066f44..4c0da302fbb 100644
--- a/packages/ra-core/src/fetch/Query.spec.tsx
+++ b/packages/ra-core/src/fetch/Query.spec.tsx
@@ -10,20 +10,17 @@ import expect from 'expect';
import Query from './Query';
import CoreAdmin from '../CoreAdmin';
import Resource from '../Resource';
+import renderWithRedux from '../util/renderWithRedux';
import TestContext from '../util/TestContext';
describe('Query', () => {
afterEach(cleanup);
it('should render its child', () => {
- const { getByTestId } = render(
-
- {() => (
-
- {() => Hello
}
-
- )}
-
+ const { getByTestId } = renderWithRedux(
+
+ {() => Hello
}
+
);
expect(getByTestId('test').textContent).toBe('Hello');
});
@@ -32,22 +29,13 @@ describe('Query', () => {
let dispatchSpy;
const myPayload = {};
act(() => {
- render(
-
- {({ store }) => {
- dispatchSpy = jest.spyOn(store, 'dispatch');
- return (
-
- {() => Hello
}
-
- );
- }}
-
+ const result = renderWithRedux(
+
+ {() => Hello
}
+
);
+
+ dispatchSpy = result.dispatch;
});
const action = dispatchSpy.mock.calls[0][0];
@@ -59,22 +47,12 @@ describe('Query', () => {
it('should set the loading state to loading when mounting', () => {
const myPayload = {};
- const { getByText } = render(
-
- {() => (
-
- {({ loading }) => (
-
- Hello
-
- )}
-
+ const { getByText } = renderWithRedux(
+
+ {({ loading }) => (
+ Hello
)}
-
+
);
expect(getByText('Hello').className).toEqual('loading');
});
diff --git a/packages/ra-core/src/i18n/useTranslate.spec.tsx b/packages/ra-core/src/i18n/useTranslate.spec.tsx
index eaad1d0163f..2c5ef6fbf9b 100644
--- a/packages/ra-core/src/i18n/useTranslate.spec.tsx
+++ b/packages/ra-core/src/i18n/useTranslate.spec.tsx
@@ -4,7 +4,7 @@ import { render, cleanup } from 'react-testing-library';
import useTranslate from './useTranslate';
import { TranslationContext } from './TranslationContext';
-import { TestContext } from '../util';
+import { renderWithRedux } from '../util';
describe('useTranslate', () => {
afterEach(cleanup);
@@ -32,11 +32,9 @@ describe('useTranslate', () => {
});
it('should use the messages set in the store', () => {
- const { queryAllByText } = render(
-
-
-
- );
+ const { queryAllByText } = renderWithRedux(, {
+ i18n: { messages: { hello: 'bonjour' } },
+ });
expect(queryAllByText('hello')).toHaveLength(0);
expect(queryAllByText('bonjour')).toHaveLength(1);
});
diff --git a/packages/ra-core/src/util/FieldTitle.spec.tsx b/packages/ra-core/src/util/FieldTitle.spec.tsx
index 2a6379ee41e..e0bf51ee35f 100644
--- a/packages/ra-core/src/util/FieldTitle.spec.tsx
+++ b/packages/ra-core/src/util/FieldTitle.spec.tsx
@@ -1,11 +1,10 @@
import assert from 'assert';
import expect from 'expect';
-import { shallow } from 'enzyme';
import { render, cleanup } from 'react-testing-library';
import React from 'react';
import { FieldTitle } from './FieldTitle';
-import TestContext from './TestContext';
+import renderWithRedux from './renderWithRedux';
describe('FieldTitle', () => {
afterEach(cleanup);
@@ -25,28 +24,22 @@ describe('FieldTitle', () => {
});
it('should use the label as translate key when translation is available', () => {
- const { container } = render(
-
-
-
- );
+ const { container } = renderWithRedux(, {
+ i18n: { messages: { foo: 'bar' } },
+ });
expect(container.firstChild.textContent).toEqual('bar');
});
it('should use the humanized source when given', () => {
- const { container } = render(
-
-
-
+ const { container } = renderWithRedux(
+
);
expect(container.firstChild.textContent).toEqual('Title');
});
it('should use the humanized source when given with underscores', () => {
- const { container } = render(
-
-
-
+ const { container } = renderWithRedux(
+
);
expect(container.firstChild.textContent).toEqual(
'Title with underscore'
@@ -54,10 +47,8 @@ describe('FieldTitle', () => {
});
it('should use the humanized source when given with camelCase', () => {
- const { container } = render(
-
-
-
+ const { container } = renderWithRedux(
+
);
expect(container.firstChild.textContent).toEqual(
'Title with camel case'
@@ -65,16 +56,13 @@ describe('FieldTitle', () => {
});
it('should use the source and resource as translate key when translation is available', () => {
- const { container } = render(
-
-
-
+ const { container } = renderWithRedux(
+ ,
+ {
+ i18n: {
+ messages: { 'resources.posts.fields.title': 'titre' },
+ },
+ }
);
expect(container.firstChild.textContent).toEqual('titre');
});
diff --git a/packages/ra-core/src/util/TestContext.tsx b/packages/ra-core/src/util/TestContext.tsx
index 90331ed7372..3e331893d4e 100644
--- a/packages/ra-core/src/util/TestContext.tsx
+++ b/packages/ra-core/src/util/TestContext.tsx
@@ -19,7 +19,7 @@ export const defaultStore = {
};
interface Props {
- store?: object;
+ initialState?: object;
enableReducers?: boolean;
}
@@ -31,7 +31,7 @@ interface Props {
* @example
* // in an enzyme test
* const wrapper = render(
- *
+ *
*
*
* );
@@ -39,7 +39,7 @@ interface Props {
* @example
* // in an enzyme test, using jest.
* const wrapper = render(
- *
+ *
* {({ store }) => {
* dispatchSpy = jest.spyOn(store, 'dispatch');
* return
@@ -52,14 +52,14 @@ class TestContext extends Component {
constructor(props) {
super(props);
- const { store = {}, enableReducers = false } = props;
+ const { initialState = {}, enableReducers = false } = props;
this.storeWithDefault = enableReducers
? createAdminStore({
- initialState: merge(defaultStore, store),
+ initialState: merge(defaultStore, initialState),
dataProvider: () => Promise.resolve({}),
history: createMemoryHistory(),
})
- : createStore(() => merge(defaultStore, store));
+ : createStore(() => merge(defaultStore, initialState));
}
renderChildren = () => {
diff --git a/packages/ra-core/src/util/index.ts b/packages/ra-core/src/util/index.ts
index 03bc482572f..1213f70929c 100644
--- a/packages/ra-core/src/util/index.ts
+++ b/packages/ra-core/src/util/index.ts
@@ -8,6 +8,7 @@ import removeEmpty from './removeEmpty';
import removeKey from './removeKey';
import resolveRedirectTo from './resolveRedirectTo';
import TestContext from './TestContext';
+import renderWithRedux from './renderWithRedux';
import warning from './warning';
export {
@@ -21,5 +22,6 @@ export {
removeKey,
resolveRedirectTo,
TestContext,
+ renderWithRedux,
warning,
};
diff --git a/packages/ra-core/src/util/renderWithRedux.tsx b/packages/ra-core/src/util/renderWithRedux.tsx
new file mode 100644
index 00000000000..a5e84a8947c
--- /dev/null
+++ b/packages/ra-core/src/util/renderWithRedux.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { render } from 'react-testing-library';
+
+import TestContext from './TestContext';
+
+/**
+ * render with react-testing library adding redux context for unit test.
+ * @example
+ * const { dispatch, reduxStore, ...otherReactTestingLibraryHelper } = renderWithRedux(
+ * ,
+ * initialState
+ * );
+ *
+ * @param {ReactNode} component: The component you want to test in jsx
+ * @param {Object} initialstate: Optional initial state of the redux store
+ * @return {{ dispatch, reduxStore, ...rest }} helper function to test rendered component.
+ * Same as react-testing-library render method with added dispatch and reduxStore helper
+ * dispatch: spy on the redux stroe dispatch method
+ * reduxStore: the redux store used by the tested component
+ */
+export default (component, initialState = {}) => {
+ let dispatch;
+ let reduxStore;
+ const renderResult = render(
+
+ {({ store }) => {
+ dispatch = jest.spyOn(store, 'dispatch');
+ reduxStore = store;
+ return component;
+ }}
+
+ );
+
+ return {
+ ...renderResult,
+ dispatch,
+ reduxStore,
+ };
+};
diff --git a/packages/ra-ui-materialui/src/detail/Create.spec.js b/packages/ra-ui-materialui/src/detail/Create.spec.js
index 8d54b2ef92f..e06a72e791c 100644
--- a/packages/ra-ui-materialui/src/detail/Create.spec.js
+++ b/packages/ra-ui-materialui/src/detail/Create.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import expect from 'expect';
import { render, cleanup } from 'react-testing-library';
-import { TestContext } from 'ra-core';
+import { renderWithRedux } from 'ra-core';
import Create from './Create';
@@ -19,12 +19,10 @@ describe('', () => {
it('should display aside component', () => {
const Dummy = () => ;
const Aside = () => Hello
;
- const { queryAllByText } = render(
-
- }>
-
-
-
+ const { queryAllByText } = renderWithRedux(
+ }>
+
+
);
expect(queryAllByText('Hello')).toHaveLength(1);
});
diff --git a/packages/ra-ui-materialui/src/detail/Edit.spec.js b/packages/ra-ui-materialui/src/detail/Edit.spec.js
index 9979d7f6437..9949fb6b7ca 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.spec.js
+++ b/packages/ra-ui-materialui/src/detail/Edit.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import expect from 'expect';
import { render, cleanup } from 'react-testing-library';
-import { TestContext } from 'ra-core';
+import { renderWithRedux } from 'ra-core';
import Edit from './Edit';
@@ -18,12 +18,10 @@ describe('', () => {
it('should display aside component', () => {
const Aside = () => Hello
;
- const { queryAllByText } = render(
-
- }>
-
-
-
+ const { queryAllByText } = renderWithRedux(
+ }>
+
+
);
expect(queryAllByText('Hello')).toHaveLength(1);
});
diff --git a/packages/ra-ui-materialui/src/detail/Show.spec.js b/packages/ra-ui-materialui/src/detail/Show.spec.js
index 632cf9c655e..655b7d33f8a 100644
--- a/packages/ra-ui-materialui/src/detail/Show.spec.js
+++ b/packages/ra-ui-materialui/src/detail/Show.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import expect from 'expect';
-import { render, cleanup } from 'react-testing-library';
-import { TestContext } from 'ra-core';
+import { cleanup } from 'react-testing-library';
+import { renderWithRedux } from 'ra-core';
import Show from './Show';
@@ -18,12 +18,10 @@ describe('', () => {
it('should display aside component', () => {
const Aside = () => Hello
;
- const { queryAllByText } = render(
-
- }>
-
-
-
+ const { queryAllByText } = renderWithRedux(
+ }>
+
+
);
expect(queryAllByText('Hello')).toHaveLength(1);
});
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.js b/packages/ra-ui-materialui/src/field/ReferenceField.js
index 2d0f4ed0113..895b469940b 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.js
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.js
@@ -2,7 +2,7 @@ import React, { Children } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles, createStyles } from '@material-ui/core/styles';
-import { ReferenceFieldController } from 'ra-core';
+import { useReference } from 'ra-core';
import LinearProgress from '../layout/LinearProgress';
import Link from '../Link';
@@ -120,15 +120,16 @@ const ReferenceField = ({ children, ...props }) => {
throw new Error(' only accepts a single child');
}
+ const { isLoading, referenceRecord, resourceLinkPath } = useReference(props);
+
return (
-
- {controllerProps => (
-
- )}
-
+
);
};
diff --git a/packages/ra-ui-materialui/src/field/SelectField.spec.js b/packages/ra-ui-materialui/src/field/SelectField.spec.js
index e4e4ea72336..c1e66c84e9b 100644
--- a/packages/ra-ui-materialui/src/field/SelectField.spec.js
+++ b/packages/ra-ui-materialui/src/field/SelectField.spec.js
@@ -2,7 +2,7 @@ import React from 'react';
import expect from 'expect';
import { render, cleanup } from 'react-testing-library';
-import { TestContext } from 'ra-core';
+import { renderWithRedux } from 'ra-core';
import { SelectField } from './SelectField';
describe('', () => {
@@ -111,24 +111,22 @@ describe('', () => {
});
it('should translate the choice by default', () => {
- const { queryAllByText } = render(
-
-
-
+ const { queryAllByText } = renderWithRedux(
+ ,
+ { i18n: { messages: { hello: 'bonjour' } } }
);
expect(queryAllByText('hello')).toHaveLength(0);
expect(queryAllByText('bonjour')).toHaveLength(1);
});
it('should not translate the choice if translateChoice is false', () => {
- const { queryAllByText } = render(
-
-
-
+ const { queryAllByText } = renderWithRedux(
+ ,
+ { i18n: { messages: { hello: 'bonjour' } } }
);
expect(queryAllByText('hello')).toHaveLength(1);
expect(queryAllByText('bonjour')).toHaveLength(0);
diff --git a/packages/ra-ui-materialui/src/list/FilterForm.spec.js b/packages/ra-ui-materialui/src/list/FilterForm.spec.js
index ad5441dfe66..a5858893401 100644
--- a/packages/ra-ui-materialui/src/list/FilterForm.spec.js
+++ b/packages/ra-ui-materialui/src/list/FilterForm.spec.js
@@ -1,7 +1,7 @@
import expect from 'expect';
import { render, cleanup } from 'react-testing-library';
import React from 'react';
-import { TestContext } from 'ra-core';
+import { renderWithRedux } from 'ra-core';
import FilterForm, { mergeInitialValuesWithDefaultValues } from './FilterForm';
import TextInput from '../input/TextInput';
@@ -20,16 +20,12 @@ describe('', () => {
const filters = []; // eslint-disable-line react/jsx-key
const displayedFilters = { title: true };
- const { queryAllByLabelText } = render(
-
- {() => (
-
- )}
-
+ const { queryAllByLabelText } = renderWithRedux(
+
);
expect(queryAllByLabelText('Title')).toHaveLength(1);
cleanup();
diff --git a/packages/ra-ui-materialui/src/list/List.spec.js b/packages/ra-ui-materialui/src/list/List.spec.js
index a1cf51c66cf..a6fab61c43f 100644
--- a/packages/ra-ui-materialui/src/list/List.spec.js
+++ b/packages/ra-ui-materialui/src/list/List.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import expect from 'expect';
-import { render, cleanup } from 'react-testing-library';
-import { TestContext } from 'ra-core';
+import { cleanup } from 'react-testing-library';
+import { renderWithRedux, TestContext } from 'ra-core';
import List, { ListView } from './List';
@@ -28,12 +28,10 @@ describe('
', () => {
it('should render a list page', () => {
const Datagrid = () => datagrid
;
- const { container } = render(
-
-
-
-
-
+ const { container } = renderWithRedux(
+
+
+
);
expect(container.querySelectorAll('.list-page')).toHaveLength(1);
});
@@ -42,16 +40,14 @@ describe('
', () => {
const Filters = () => filters
;
const Pagination = () => pagination
;
const Datagrid = () => datagrid
;
- const { queryAllByText, debug } = render(
-
- }
- pagination={}
- {...defaultProps}
- >
-
-
-
+ const { queryAllByText, debug } = renderWithRedux(
+ }
+ pagination={}
+ {...defaultProps}
+ >
+
+
);
expect(queryAllByText('filters')).toHaveLength(2);
expect(queryAllByText('Export')).toHaveLength(1);
@@ -73,7 +69,7 @@ describe('
', () => {
total: 0,
};
- const defaultStoreForList = {
+ const defaultStateForList = {
admin: {
resources: {
foo: {
@@ -91,12 +87,11 @@ describe('
', () => {
it('should display aside component', () => {
const Dummy = () => ;
const Aside = () => Hello
;
- const { queryAllByText } = render(
-
-
}>
-
-
-
+ const { queryAllByText } = renderWithRedux(
+
}>
+
+ ,
+ defaultStateForList
);
expect(queryAllByText('Hello')).toHaveLength(1);
});