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