From 0e65e84859c19256bc05e2b285d6faefb056fe3f Mon Sep 17 00:00:00 2001 From: Brandon Orther Date: Tue, 22 May 2018 13:31:29 -0700 Subject: [PATCH] feat: Initial working createResource ResourceRequester component --- .../resource/components/ResourceRequester.js | 123 ++++++++++++++ .../__tests__/ResourceRequester.test.js | 156 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 src/helpers/resource/components/ResourceRequester.js create mode 100644 src/helpers/resource/components/__tests__/ResourceRequester.test.js diff --git a/src/helpers/resource/components/ResourceRequester.js b/src/helpers/resource/components/ResourceRequester.js new file mode 100644 index 0000000..9118cde --- /dev/null +++ b/src/helpers/resource/components/ResourceRequester.js @@ -0,0 +1,123 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import { + resourceCreateRequest, + // resourceUpdateRequest, + // resourceDeleteRequest, +} from '../../../modules/resource/actions' + +class ResourceRequester extends React.Component { + state = { + requestResult: null, + error: null, + loading: false, + } + + getRequesResultValuesObj() { + const {requestResult} = this.state + const entities = requestResult && requestResult.entities + const result = requestResult && requestResult.data + + return {entities, requestResult, result} + } + + getStatusObj() { + const error = !!this.state.error + const loading = this.state.loading + const success = !!this.state.requestResult && !loading + const initial = !error && !loading && !success + + return {error, initial, loading, success} + } + + createResource = data => { + this.setState({loading: true}) + this.requestResourceCreate(data).then( + this.requestResourceSuccess, + this.requestResourceError, + ) + } + + requestResourceError = error => { + this.setState({loading: false, error}) + } + + requestResourceSuccess = payload => { + const requestResult = { + api: payload.api, + data: payload.data, + entities: payload.entities, + entityType: payload.entityType, + resource: payload.resource, + } + + this.setState({ + requestResult, + error: null, + loading: false, + }) + } + + requestResourceCreate = data => { + const {requestCreate, resource, entityType} = this.props + return requestCreate(resource, data, entityType) + } + + // requestResourceDelete = data => { + // const {requestDelete, resource, entityType} = this.props + // return requestDelete(resource, data, entityType) + // } + + // requestResourceUpdate = data => { + // const {requestUpdate, resource, entityType} = this.props + // return requestUpdate(resource, data, entityType) + // } + + resetState = () => { + this.setState({ + requestResult: null, + error: null, + loading: false, + }) + } + + render() { + if (typeof this.props.children !== 'function') { + throw new Error('Children should be a Function!') + } + + const {error} = this.state + const {entityType} = this.props + const {result, ...requestResultValues} = this.getRequesResultValuesObj() + + return this.props.children({ + createResource: this.createResource, + status: this.getStatusObj(), + error, + entityType, + result, + ...requestResultValues, + }) + } +} + +ResourceRequester.propTypes = { + children: PropTypes.func.isRequired, + entityType: PropTypes.string, + // list: PropTypes.bool.isRequired, + requestCreate: PropTypes.func, + // requestDelete: PropTypes.func, + // requestUpdate: PropTypes.func, + // requestParams: PropTypes.object, + resource: PropTypes.string.isRequired, + // resourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +} + +const mapDispatchToProps = { + requestCreate: resourceCreateRequest, + // requestDelete: resourceDeleteRequest, + // requestUpdate: resourceUpdateRequest, +} + +export default connect(null, mapDispatchToProps)(ResourceRequester) diff --git a/src/helpers/resource/components/__tests__/ResourceRequester.test.js b/src/helpers/resource/components/__tests__/ResourceRequester.test.js new file mode 100644 index 0000000..5a1f3b0 --- /dev/null +++ b/src/helpers/resource/components/__tests__/ResourceRequester.test.js @@ -0,0 +1,156 @@ +import React from 'react' +import MockAdapter from 'axios-mock-adapter' +import createSagaMiddleware from 'redux-saga' +import {Provider} from 'react-redux' +import {createStore, applyMiddleware, combineReducers} from 'redux' +import {render, Simulate, wait} from 'react-testing-library' +import {schema} from 'normalizr' +import ReduxThunk from 'redux-thunk' +// eslint-disable-next-line +import 'dom-testing-library/extend-expect' +import {middleware as ReduxSagaThunk} from 'redux-saga-thunk' +import ResourceRequester from '../ResourceRequester' +import {createResource, createEntities} from '../../../../' +import API from '../../../../utils/test/api' + +const {api, axiosInstance} = API.create() +const mockApi = new MockAdapter(axiosInstance, {delayResponse: 300}) + +function createStoreForTests(initialState) { + const resource = createResource() + const entities = createEntities({ + isDevEnv: false, + schemas: { + user: new schema.Entity('user', {}, {idAttribute: 'uuid'}), + team: new schema.Entity('team', {idAttribute: 'id'}), + }, + }) + const reducer = combineReducers({ + entities: entities.reducer, + resource: resource.reducer, + }) + const sagas = resource.sagas + const sagaMiddleware = createSagaMiddleware() + const store = createStore( + reducer, + initialState, + applyMiddleware(ReduxSagaThunk, ReduxThunk, sagaMiddleware), + ) + sagaMiddleware.run(sagas, {api}) + return store +} + +function renderWithRedux( + ui, + {initialState, store = createStoreForTests(initialState)} = {}, +) { + return { + ...render({ui}), + // adding `store` to the returned utilities to allow us + // to reference it in our tests (just try to avoid using + // this to test implementation details). + store, + } +} + +// eslint-disable-next-line react/prop-types +const Status = ({children, initial, loading, error, success}) => { + if (initial) return
Initial
+ if (loading) return
Loading
+ if (error) return
{children}
+ if (success) return
{children}
+ throw new Error('Status component not passed status prop') +} + +test('ResourceRequester component receives props and renders initial status', () => { + // Renders ResourceRequester component with statusView from renderInitial prop. + const {getByTestId} = renderWithRedux( + + {({status}) =>
{status.initial && }
} +
, + ) + + // Expects ResourceRequester component to render statusView from renderInitial. + expect(getByTestId('render-initial')).toHaveTextContent('Initial') +}) + +test('ResourceRequester POSTs when createResource called and renders results', async () => { + const newUserData = {id: 1, name: 'Ben'} + mockApi.onPost('/user').reply(201, {...newUserData}) + + // Renders ResourceRequester component with statusView from renderInitial prop. + const {getByTestId, getByText} = renderWithRedux( + + {({status, createResource: createUser, result: user}) => ( +
+
+ +
+ {status.initial && } + {status.success && ( + +
+ {user.id}:{user.name} +
+
+ )} +
+ )} +
, + ) + + // Expects ResourceRequester component to render statusView from renderInitial. + expect(getByTestId('render-initial')).toHaveTextContent('Initial') + + // Simulating a button click in the browser + Simulate.click(getByText('Create User')) + + // Expects ResourceRequester component to render statusView from renderLoading. + await wait(() => expect(getByTestId('render-success')).toBeInTheDOM()) +}) + +// test('ResourceRequester loads with params passed in', async () => { +// mockApi +// .onGet('/dude', {params: {best: 'dude'}}) +// .reply(200, [{id: 1, name: 'Ben'}, {id: 2, name: 'Sam'}]) + +// // Renders ResourceRequester component with statusView from renderInitial prop. +// const {getByTestId} = renderWithRedux( +// } +// renderError={error => {error}} +// renderLoading={() => } +// renderSuccess={userList => ( +// +// {userList.map(user => ( +//
+// {user.id}:{user.name} +//
+// ))} +//
+// )} +// requestParams={{best: 'dude'}} +// loadOnMount +// list +// > +// {({statusView}) =>
{statusView}
} +//
, +// ) + +// // Expects ResourceRequester component to render statusView from renderLoading. +// expect(getByTestId('render-loading')).toHaveTextContent('Loading') + +// // expect(api.get).toHaveBeenCalled() +// // console.log(mockApi.history.get) +// // // Expects ResourceRequester component to render statusView from renderSuccess. +// await wait(() => expect(getByTestId('render-success')).toBeInTheDOM()) +// })