diff --git a/.babelrc b/.babelrc index 5557047..0938dbc 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,12 @@ { - "presets": ["@babel/preset-env","@babel/preset-react"], - "plugins": ["transform-class-properties"] + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "transform-class-properties", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-regenerator", + "@babel/plugin-transform-runtime" + ] } diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..80ed262 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,10 @@ +version: "2" +checks: + method-lines: + config: + threshold: 40 + method-complexity: + config: + threshold: 6 +exclude_patterns: +- "**/__tests__/" diff --git a/.env.example b/.env.example index 7c86a1e..9f6eba3 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,8 @@ #TO RUN THE SERVER -PORT= #ADD PORT NUMBER HERE +REACT_APP_PORT= #APP PORT_NUMBER GOES HERE + +# API BASE URL +REACT_APP_API_URL= #BACKEND API BASE URL eg: https://bft-nmd-stag.herokuapp.com/api + +# TESTING TOKEN +REACT_APP_SOME_TOKEN= # JWT_TOKEN eg: eyJhbGciO.... diff --git a/.eslintrc.json b/.eslintrc.json index 2617ee6..0c443aa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,9 @@ } ], "react/no-array-index-key": 0, - "jsx-a11y/anchor-is-valid": 0 + "jsx-a11y/anchor-is-valid": 0, + "no-shadow": 0, + "no-nested-ternary": 0, + "react/jsx-props-no-spreading": 0 } } diff --git a/jest.config.js b/jest.config.js index 17df3a6..90d007b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { '!src/entry/server.js', '!src/utils/customMessages.js', '!webpack.config.js', + '!src/assets/**', ], coverageDirectory: 'coverage', coveragePathIgnorePatterns: ['/node_modules/', '/__tests__'], diff --git a/package.json b/package.json index 467f554..a70cf75 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,19 @@ "homepage": "https://github.com/Stackup-Rwanda/stackup2-barefoot-frontend#readme", "dependencies": { "@babel/core": "^7.9.6", + "@babel/plugin-transform-runtime": "^7.10.1", + "@babel/polyfill": "^7.10.1", + "@babel/preset-env": "^7.10.2", "@babel/preset-react": "^7.9.4", "@material-ui/core": "^4.10.0", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.55", "@storybook/addon-actions": "^5.3.18", "@storybook/addon-links": "^5.3.18", "@storybook/addons": "^5.3.18", "@storybook/react": "^5.3.18", "@testing-library/react": "^10.0.4", + "axios": "^0.19.2", "babel-eslint": "^10.1.0", "babel-jest": "^26.0.1", "babel-loader": "^8.1.0", @@ -59,29 +64,35 @@ "jest": "^26.0.1", "jest-html-reporters": "^1.2.1", "jest-transform-stub": "^2.0.0", + "less-loader": "^6.1.0", "mini-css-extract-plugin": "^0.9.0", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-loader-spinner": "^3.1.14", "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", + "react-slick": "^0.26.1", "react-test-renderer": "^16.13.1", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.4", "redux-thunk": "^2.3.0", + "slick-carousel": "^1.8.1", "style-loader": "^1.2.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0" }, "devDependencies": { + "dotenv-webpack": "^1.8.0", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.1.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react": "^7.20.0", "eslint-plugin-react-hooks": "^2.5.1", + "moxios": "^0.4.0", "node-sass": "^4.14.1", "sass": "^1.26.5", "sass-loader": "^8.0.2" diff --git a/src/__tests__/App.test.js b/src/__tests__/App.test.js deleted file mode 100644 index 3edf03d..0000000 --- a/src/__tests__/App.test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable no-undef */ -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { Provider } from 'react-redux'; -import renderer from 'react-test-renderer'; -import configureStore from 'redux-mock-store'; -import App from '../entry/App'; -import LandingPage from '../views/LandingPage/LandingPage'; -import reducer from '../reducers/reducer'; -import firstMessage from '../actions/actions'; - -const mockStore = configureStore([]); -const store = mockStore({ - message: 'Welcome', -}); -store.dispatch = jest.fn(); -const component = renderer.create( - - - , -); -describe('App tests', () => { - it('Will prove that the app is rendered from App component', () => { - const appRender = shallow(); - expect(appRender.contains()); - }); - - it('should return welcome when no action provided', () => { - expect(reducer(undefined, {})).toEqual({ message: 'Welcome' }); - }); - it('should return Redux when action is provided with value', () => { - expect(reducer(undefined, { ...firstMessage, value: 'Redux' })).toEqual({ message: 'Redux' }); - }); -}); diff --git a/src/__tests__/data/data.js b/src/__tests__/data/data.js new file mode 100644 index 0000000..9ff6949 --- /dev/null +++ b/src/__tests__/data/data.js @@ -0,0 +1,32 @@ +const requests = { + tripRequests: [ + { + id: 1, + trips: [ + { + travelDate: '2020-09-15', + travelFrom: 'Kigali', + travelTo: 'Durban', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + { + id: 2, + trips: [ + { + travelDate: '2020-09-10', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + ], +}; + +export default requests; diff --git a/src/actions/actions.js b/src/actions/actions.js deleted file mode 100644 index 780bf61..0000000 --- a/src/actions/actions.js +++ /dev/null @@ -1,5 +0,0 @@ -const firstMessage = { - type: 'FIRST_MESSAGE', -}; - -export default firstMessage; diff --git a/src/assets/images/BarefootNomad.svg b/src/assets/images/BarefootNomad.svg new file mode 100644 index 0000000..a47a6be --- /dev/null +++ b/src/assets/images/BarefootNomad.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/dorsey.svg b/src/assets/images/dorsey.svg new file mode 100644 index 0000000..cccae9a --- /dev/null +++ b/src/assets/images/dorsey.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/styles/sass/index.scss b/src/assets/styles/sass/index.scss index 8501e4f..8512c3e 100644 --- a/src/assets/styles/sass/index.scss +++ b/src/assets/styles/sass/index.scss @@ -43,10 +43,28 @@ body { align-items: center; } +.brand-link { + font-size: 18pt; + font-weight: 700; + text-decoration: none !important; + + &:hover { + text-decoration: none !important; + } + + &:focus { + text-decoration: none !important; + } +} + .brand-barefoot { color: $secondary; } +.brand-nomad { + color: $background-light; +} + .navlink-signup { margin-left: 16px !important; } @@ -137,21 +155,41 @@ body { } .testimony-item { - display: flex; + display: flex !important; flex-direction: row; justify-content: center; align-items: center; } +.slick-arrow { + display: none !important; +} + .testimony-reviewer { - background: url(../../images/reviewer.svg); background-position: center; - background-repeat: no-repeat; + background-repeat: no-repeat !important; background-size: cover; width: 298px; min-height: 485px; } +.musk { + background: url(../../images/reviewer.svg); +} + +.dorsey { + background: url(../../images/dorsey.svg); +} + +.mastermind { + display: none; +} + +.mastermind-content { + min-height: 485px; + justify-content: center !important; +} + .testimony-content { display: flex; flex-direction: column; @@ -169,6 +207,10 @@ body { font-size: 14pt; } +.testimony-description { + margin-left: 2px; +} + .testimony-author { font-size: 10pt; } @@ -290,6 +332,19 @@ body { } @media (max-width: 575.98px) { + .brand-link { + font-size: 16pt; + font-weight: 700; + text-decoration: none !important; + &:hover { + text-decoration: none !important; + } + + &:focus { + text-decoration: none !important; + } + } + .description-wrapper { width: 100%; margin-left: 0; @@ -303,12 +358,24 @@ body { flex-direction: column; } + .slick-list { + padding: 0 !important; + } + + .slick-active { + width: 360px !important; + } + .testimony-content { flex-direction: column; justify-content: center; margin: 24px 0 24px 0; } + .testimony-description { + font-size: 12pt; + } + .socials { width: 50%; } diff --git a/src/components/CustomCard/__tests__/CustomCard.test.js b/src/components/CustomCard/__tests__/CustomCard.test.js new file mode 100644 index 0000000..a567053 --- /dev/null +++ b/src/components/CustomCard/__tests__/CustomCard.test.js @@ -0,0 +1,22 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import { createMount } from '@material-ui/core/test-utils'; +import Link from '@material-ui/core/Link'; +import { cleanup } from '@testing-library/react'; +import CustomCard from '../CustomCard'; + +const title = 'Our Company'; +const items = ['Careers', 'About', 'Vision']; + +describe('', () => { + afterEach(() => cleanup); + + it('Should match the CustomCard snapshot', () => { + const component = createMount()( + , + ); + + expect(component.html).toMatchSnapshot(); + expect(component.find(Link)).toHaveLength(3); + }); +}); diff --git a/src/components/CustomCard/__tests__/__snapshots__/CustomCard.test.js.snap b/src/components/CustomCard/__tests__/__snapshots__/CustomCard.test.js.snap new file mode 100644 index 0000000..d675497 --- /dev/null +++ b/src/components/CustomCard/__tests__/__snapshots__/CustomCard.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the CustomCard snapshot 1`] = `[Function]`; diff --git a/src/components/RequestsTable/RequestsTable.js b/src/components/RequestsTable/RequestsTable.js new file mode 100644 index 0000000..bca3187 --- /dev/null +++ b/src/components/RequestsTable/RequestsTable.js @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes, { object } from 'prop-types'; +import { withStyles, makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ActionsMenu from '../../views/ActionsMenu/ActionsMenu'; + +const StyledTableCell = withStyles((theme) => ({ + head: { + backgroundColor: theme.palette.secondary.contrastText, + color: theme.palette.primary.dark, + }, + body: { + fontSize: 14, + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme) => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + }, +}))(TableRow); + +const useStyles = makeStyles({ + table: { + minWidth: 700, + }, + tableCell: { + fontSize: 12, + }, + tableIcons: { + fontSize: 16, + }, + noData: { + fontSize: 14, + color: '#777777', + backgroundColor: '#FFFFFF', + textAlign: 'center', + }, +}); + +const requesterActions = [ + { + id: 1, + name: 'Edit', + }, + { + id: 2, + name: 'Book Accommodation', + }, + { + id: 3, + name: 'Rate Accommodation', + }, + { + id: 4, + name: 'Like/Dislike Accommodation', + }, +]; + +const RequestsTable = ({ requests }) => { + const classes = useStyles(); + + return ( + + + + + Date + Departure + Destination + Reason + Accommodation + Status + + + + + {requests.length > 0 ? (requests.map((row) => ( + + + {row.trips[0].travelDate} + + {row.trips[0].travelFrom} + {row.trips[0].travelTo} + {row.travelReason} + {row.accommodation ? 'Yes' : 'No'} + + {row.status === 'pending' ? : } + + + + + + )) + ) : ( + + + Oops! Looks like you have not yet made any requests. + + + )} + +
+
+ ); +}; + +RequestsTable.propTypes = { + requests: PropTypes.arrayOf(object).isRequired, +}; + +export default RequestsTable; diff --git a/src/components/RequestsTable/__tests__/RequestsTable.test.js b/src/components/RequestsTable/__tests__/RequestsTable.test.js new file mode 100644 index 0000000..6b418e7 --- /dev/null +++ b/src/components/RequestsTable/__tests__/RequestsTable.test.js @@ -0,0 +1,78 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import { createMount } from '@material-ui/core/test-utils'; +import { cleanup } from '@testing-library/react'; +import RequestsTable from '../RequestsTable'; + +const requests = [ + { + id: 1, + trips: [ + { + travelDate: '2020-09-15', + travelFrom: 'Kigali', + travelTo: 'Durban', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + { + id: 2, + trips: [ + { + travelDate: '2020-09-10', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + { + id: 3, + trips: [ + { + travelDate: '2020-09-11', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: false, + status: 'pending', + }, + { + id: 4, + trips: [ + { + travelDate: '2020-09-12', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: false, + status: 'accepted', + }, +]; + +describe('', () => { + afterEach(() => cleanup); + + it('Should match the RequestsTable snapshot', () => { + const component = createMount()( + , + ); + expect(component.html).toMatchSnapshot(); + }); + + it('Should match the RequestsTable snapshot when no requests', () => { + const component = createMount()( + , + ); + expect(component.html).toMatchSnapshot(); + }); +}); diff --git a/src/components/RequestsTable/__tests__/__snapshots__/RequestsTable.test.js.snap b/src/components/RequestsTable/__tests__/__snapshots__/RequestsTable.test.js.snap new file mode 100644 index 0000000..739de42 --- /dev/null +++ b/src/components/RequestsTable/__tests__/__snapshots__/RequestsTable.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the RequestsTable snapshot 1`] = `[Function]`; + +exports[` Should match the RequestsTable snapshot when no requests 1`] = `[Function]`; diff --git a/src/components/SectionHeader/SectionHeader.js b/src/components/SectionHeader/SectionHeader.js new file mode 100644 index 0000000..b59d777 --- /dev/null +++ b/src/components/SectionHeader/SectionHeader.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles({ + title: { + color: '#484848', + backgroundColor: '#00a7990f', + padding: 24, + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: 500, + }, +}); + +const SectionHeader = ({ title }) => { + const classes = useStyles(); + + return ( + + {title} + + ); +}; + +SectionHeader.propTypes = { + title: PropTypes.string.isRequired, +}; + +export default SectionHeader; diff --git a/src/components/SectionHeader/__tests__/SectionHeader.test.js b/src/components/SectionHeader/__tests__/SectionHeader.test.js new file mode 100644 index 0000000..991f1a7 --- /dev/null +++ b/src/components/SectionHeader/__tests__/SectionHeader.test.js @@ -0,0 +1,16 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; +import SectionHeader from '../SectionHeader'; + +const title = 'Trip requests'; + +describe('', () => { + afterEach(() => cleanup); + + it('Should match the SectionHeader component snapshot', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/SectionHeader/__tests__/__snapshots__/SectionHeader.test.js.snap b/src/components/SectionHeader/__tests__/__snapshots__/SectionHeader.test.js.snap new file mode 100644 index 0000000..5d1973b --- /dev/null +++ b/src/components/SectionHeader/__tests__/__snapshots__/SectionHeader.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the SectionHeader component snapshot 1`] = ` +

+ Trip requests +

+`; diff --git a/src/entry/App.js b/src/entry/App.js index 2f18277..74b5f39 100644 --- a/src/entry/App.js +++ b/src/entry/App.js @@ -5,6 +5,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'; import theme from '../assets/styles/theme'; import LandingPage from '../views/LandingPage/LandingPage'; import NavBar from '../views/NavBar/NavBar'; +import RequestsTablePage from '../views/RequestsTablePage/RequestsTablePage'; const App = () => ( @@ -12,7 +13,8 @@ const App = () => ( - + + diff --git a/src/entry/__tests__/App.test.js b/src/entry/__tests__/App.test.js new file mode 100644 index 0000000..7c7f032 --- /dev/null +++ b/src/entry/__tests__/App.test.js @@ -0,0 +1,31 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { applyMiddleware, createStore } from 'redux'; +import thunk from 'redux-thunk'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { shallow } from 'enzyme'; +import rootReducer from '../../redux/rootReducer'; +import App from '../App'; + +const middlewares = [thunk]; + +const testStore = (state) => { + const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore); + return createStoreWithMiddleware(rootReducer, state); +}; + +const store = testStore({}); + +describe('', () => { + it('Should render the App component', () => { + const wrapper = shallow( + + + , + ); + expect(wrapper.contains()); + expect(wrapper.contains()); + }); +}); diff --git a/src/entry/index.js b/src/entry/index.js index cdff617..cf8838e 100644 --- a/src/entry/index.js +++ b/src/entry/index.js @@ -1,14 +1,13 @@ - import React from 'react'; import ReactDOM from 'react-dom'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; import App from './App'; import '../assets/styles/sass/index.scss'; import '../assets/styles/css/index.css'; -import reducer from '../reducers/reducer'; - -const store = createStore(reducer, composeWithDevTools()); +import store from '../redux/store'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , document.getElementById('root'), +); diff --git a/src/reducers/reducer.js b/src/reducers/reducer.js deleted file mode 100644 index 0fbe134..0000000 --- a/src/reducers/reducer.js +++ /dev/null @@ -1,17 +0,0 @@ -import firstMessage from '../actions/actions'; - -const initialState = { - message: 'Welcome', -}; -const reducer = (state = initialState, action) => { - switch (action.type) { - case firstMessage.type: - return { - message: `${action.value}`, - }; - default: - return state; - } -}; - -export default reducer; diff --git a/src/redux/rootReducer.js b/src/redux/rootReducer.js new file mode 100644 index 0000000..f0d7080 --- /dev/null +++ b/src/redux/rootReducer.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import tripRequestsReducer from './tripRequests/tripRequestsReducer'; + +const rootReducer = combineReducers({ + tripRequests: tripRequestsReducer, +}); + +export default rootReducer; diff --git a/src/redux/store.js b/src/redux/store.js new file mode 100644 index 0000000..347851a --- /dev/null +++ b/src/redux/store.js @@ -0,0 +1,13 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import rootReducer from './rootReducer'; + +export const middlewares = [thunk]; + +const store = createStore( + rootReducer, + composeWithDevTools(applyMiddleware(...middlewares)), +); + +export default store; diff --git a/src/redux/tripRequests/__tests__/__snapshots__/tripRequestsActions.test.js.snap b/src/redux/tripRequests/__tests__/__snapshots__/tripRequestsActions.test.js.snap new file mode 100644 index 0000000..bd4d44a --- /dev/null +++ b/src/redux/tripRequests/__tests__/__snapshots__/tripRequestsActions.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TripRequests actions Dispatches the FETCH_TRIP_REQUESTS_FAILURE action 1`] = ` +Array [ + Object { + "payload": undefined, + "type": "FETCH_TRIP_REQUESTS_FAILURE", + }, +] +`; + +exports[`TripRequests actions Dispatches the FETCH_TRIP_REQUESTS_REQUEST action 1`] = ` +Array [ + Object { + "type": "FETCH_TRIP_REQUESTS_REQUEST", + }, +] +`; + +exports[`TripRequests actions Dispatches the FETCH_TRIP_REQUESTS_SUCCESS action 1`] = ` +Array [ + Object { + "payload": undefined, + "type": "FETCH_TRIP_REQUESTS_SUCCESS", + }, +] +`; diff --git a/src/redux/tripRequests/__tests__/tripRequestsActions.test.js b/src/redux/tripRequests/__tests__/tripRequestsActions.test.js new file mode 100644 index 0000000..c68835b --- /dev/null +++ b/src/redux/tripRequests/__tests__/tripRequestsActions.test.js @@ -0,0 +1,104 @@ +/* eslint-disable no-undef */ +import moxios from 'moxios'; +import configureStore from 'redux-mock-store'; +import * as actions from '../tripRequestsActions'; +import { mockStore } from '../../../utils'; + +const testStore = configureStore(); +const store = testStore(); + +describe('TripRequests actions', () => { + beforeEach(() => { + store.clearActions(); + }); + + it('Dispatches the FETCH_TRIP_REQUESTS_REQUEST action', () => { + store.dispatch(actions.fetchTripRequestsRequest()); + expect(store.getActions()).toMatchSnapshot(); + }); + + it('Dispatches the FETCH_TRIP_REQUESTS_SUCCESS action', () => { + store.dispatch(actions.fetchTripRequestsSuccess()); + expect(store.getActions()).toMatchSnapshot(); + }); + + it('Dispatches the FETCH_TRIP_REQUESTS_FAILURE action', () => { + store.dispatch(actions.fetchTripRequestsFailure()); + expect(store.getActions()).toMatchSnapshot(); + }); + + // it('Mocks the fetchTripRequests special action', async () => { + // actions.fetchTripRequests = jest.fn(() => + // store.dispatch(actions.fetchTripRequestsRequest())); + // actions.fetchTripRequests(); + // store.dispatch(actions.fetchTripRequests()); + // expect(actions.fetchTripRequests).toHaveBeenCalled(); + // }); +}); + +describe('FetchTripRequests action', () => { + beforeEach(() => { + moxios.install(actions.api); + }); + + afterEach(() => { + moxios.uninstall(actions.api); + }); + + it('Updates store successfully', () => { + const expectedState = [ + { + id: 1, + trips: [ + { + travelDate: '2020-09-15', + travelFrom: 'Kigali', + travelTo: 'Durban', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + { + id: 2, + trips: [ + { + travelDate: '2020-09-10', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + ]; + const store = mockStore(); + + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: expectedState, + }); + request.reject({ + status: 400, + response: { + status: 400, + data: { + error: 'Invalid token', + }, + }, + }); + }); + + return store.dispatch(actions.fetchTripRequests()).then(() => { + const newState = store.getState(); + expect(newState.requests).toBe(expectedState); + }).catch(() => { + const newState = store.getState(); + expect(newState.tripRequests.tripRequests).toEqual([]); + }); + }); +}); diff --git a/src/redux/tripRequests/__tests__/tripRequestsReducer.test.js b/src/redux/tripRequests/__tests__/tripRequestsReducer.test.js new file mode 100644 index 0000000..957bb1c --- /dev/null +++ b/src/redux/tripRequests/__tests__/tripRequestsReducer.test.js @@ -0,0 +1,69 @@ +/* eslint-disable no-undef */ +import tripRequestsReducer from '../tripRequestsReducer'; +import { + FETCH_TRIP_REQUESTS_REQUEST, + FETCH_TRIP_REQUESTS_SUCCESS, + FETCH_TRIP_REQUESTS_FAILURE, +} from '../tripRequestsTypes'; +import data from '../../../__tests__/data/data'; + +const { tripRequests } = data; +const error = 'Token failed!'; + +describe('Trip Requests Reducer Test INITIAL_STATE', () => { + it('is ok', () => { + const action = { type: 'something' }; + const initialState = { + loading: true, + tripRequests: [], + error: '', + }; + + expect(tripRequestsReducer(undefined, action)).toEqual(initialState); + }); +}); + +describe('Trip Requests Reducer Test FETCH_TRIP_REQUESTS_REQUEST', () => { + it('returns correct state', () => { + const action = { type: FETCH_TRIP_REQUESTS_REQUEST }; + const expectedState = { + loading: true, + tripRequests: [], + error: '', + }; + + expect(tripRequestsReducer(undefined, action)).toEqual(expectedState); + }); +}); + +describe('Trip Requests Reducer Test FETCH_TRIP_REQUESTS_SUCCESS', () => { + it('returns correct state', () => { + const action = { + type: FETCH_TRIP_REQUESTS_SUCCESS, + payload: tripRequests, + }; + const expectedState = { + loading: false, + tripRequests, + error: '', + }; + + expect(tripRequestsReducer(undefined, action)).toEqual(expectedState); + }); +}); + +describe('Trip Requests Reducer Test FETCH_TRIP_REQUESTS_FAILURE', () => { + it('returns correct state', () => { + const action = { + type: FETCH_TRIP_REQUESTS_FAILURE, + payload: error, + }; + const expectedState = { + loading: false, + tripRequests: [], + error, + }; + + expect(tripRequestsReducer(undefined, action)).toEqual(expectedState); + }); +}); diff --git a/src/redux/tripRequests/tripRequestsActions.js b/src/redux/tripRequests/tripRequestsActions.js new file mode 100644 index 0000000..d93ee69 --- /dev/null +++ b/src/redux/tripRequests/tripRequestsActions.js @@ -0,0 +1,45 @@ +import axios from 'axios'; +import { + FETCH_TRIP_REQUESTS_REQUEST, + FETCH_TRIP_REQUESTS_SUCCESS, + FETCH_TRIP_REQUESTS_FAILURE, +} from './tripRequestsTypes'; + +export const fetchTripRequestsRequest = () => ({ + type: FETCH_TRIP_REQUESTS_REQUEST, +}); + +export const fetchTripRequestsSuccess = (tripRequests) => ({ + type: FETCH_TRIP_REQUESTS_SUCCESS, + payload: tripRequests, +}); + +export const fetchTripRequestsFailure = (error) => ({ + type: FETCH_TRIP_REQUESTS_FAILURE, + payload: error, +}); + +export const api = axios.create({ + baseURL: process.env.REACT_APP_API_URL, + headers: { + Authorization: `Bearer ${process.env.REACT_APP_SOME_TOKEN}`, + 'Content-Type': 'application/json', + }, + method: 'GET', +}); + +export const fetchTripRequests = () => async (dispatch) => { + dispatch(fetchTripRequestsRequest); + await api.get('/trips') + .then((res) => { + dispatch(fetchTripRequestsSuccess(res.data.data.foundRequests)); + }).catch((err) => { + if (err.response.status === 400) { + const errorMessage = err.response.data.error; + dispatch(fetchTripRequestsFailure(errorMessage)); + } else { + const tripRequests = []; + dispatch(fetchTripRequestsSuccess(tripRequests)); + } + }); +}; diff --git a/src/redux/tripRequests/tripRequestsReducer.js b/src/redux/tripRequests/tripRequestsReducer.js new file mode 100644 index 0000000..46f6bca --- /dev/null +++ b/src/redux/tripRequests/tripRequestsReducer.js @@ -0,0 +1,36 @@ +import { + FETCH_TRIP_REQUESTS_REQUEST, + FETCH_TRIP_REQUESTS_SUCCESS, + FETCH_TRIP_REQUESTS_FAILURE, +} from './tripRequestsTypes'; + +const initialState = { + loading: true, + tripRequests: [], + error: '', +}; + +const tripRequestsReducer = (state = initialState, { type, payload }) => { + switch (type) { + case FETCH_TRIP_REQUESTS_REQUEST: + return { + ...state, + loading: true, + }; + case FETCH_TRIP_REQUESTS_SUCCESS: + return { + loading: false, + tripRequests: payload, + error: '', + }; + case FETCH_TRIP_REQUESTS_FAILURE: + return { + loading: false, + tripRequests: [], + error: payload, + }; + default: return state; + } +}; + +export default tripRequestsReducer; diff --git a/src/redux/tripRequests/tripRequestsTypes.js b/src/redux/tripRequests/tripRequestsTypes.js new file mode 100644 index 0000000..5a2b7dd --- /dev/null +++ b/src/redux/tripRequests/tripRequestsTypes.js @@ -0,0 +1,3 @@ +export const FETCH_TRIP_REQUESTS_REQUEST = 'FETCH_TRIP_REQUESTS_REQUEST'; +export const FETCH_TRIP_REQUESTS_SUCCESS = 'FETCH_TRIP_REQUESTS_SUCCESS'; +export const FETCH_TRIP_REQUESTS_FAILURE = 'FETCH_TRIP_REQUESTS_FAILURE'; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..27a8f45 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,9 @@ +/* eslint-disable import/prefer-default-export */ +import { createStore, applyMiddleware } from 'redux'; +import rootReducer from '../redux/rootReducer'; +import { middlewares } from '../redux/store'; + +export const mockStore = (initialState) => { + const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore); + return createStoreWithMiddleware(rootReducer, initialState); +}; diff --git a/src/views/ActionsMenu/ActionsMenu.js b/src/views/ActionsMenu/ActionsMenu.js new file mode 100644 index 0000000..442f7ca --- /dev/null +++ b/src/views/ActionsMenu/ActionsMenu.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes, { object } from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreVert from '@material-ui/icons/MoreVert'; + +const useStyles = makeStyles({ + tableIcons: { + fontSize: 16, + }, + item: { + fontSize: 12, + color: '#484848', + }, +}); + +const ActionsMenu = ({ actions }) => { + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const classes = useStyles(); + + return ( +
+ + + { + actions.map((action) => ( + + {action.name} + + )) + } + +
+ ); +}; + +ActionsMenu.propTypes = { + actions: PropTypes.arrayOf(object).isRequired, +}; + +export default ActionsMenu; diff --git a/src/views/ActionsMenu/__tests__/ActionsMenu.test.js b/src/views/ActionsMenu/__tests__/ActionsMenu.test.js new file mode 100644 index 0000000..d0b3c42 --- /dev/null +++ b/src/views/ActionsMenu/__tests__/ActionsMenu.test.js @@ -0,0 +1,51 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import { createMount } from '@material-ui/core/test-utils'; +import Button from '@material-ui/core/Button'; +import { cleanup } from '@testing-library/react'; +import ActionsMenu from '../ActionsMenu'; + +const actions = [ + { + id: 1, + name: 'Edit', + }, + { + id: 2, + name: 'Book Accommodation', + }, + { + id: 3, + name: 'Rate Accommodation', + }, + { + id: 4, + name: 'Like/Dislike Accommodation', + }, +]; + +describe('', () => { + afterEach(() => cleanup); + + it('Should match the ActionsMenu snapshot', () => { + const component = createMount()( + , + ); + expect(component.html).toMatchSnapshot(); + }); +}); + +describe(' onClick()', () => { + afterEach(() => cleanup); + + it('Calls the onClick handler to open', () => { + const mockedHandleClick = jest.fn(); + const component = createMount()( + , + ); + component.find(Button).first().props().onClick = mockedHandleClick; + component.find(Button).first().props().onClick(); + + expect(mockedHandleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/views/ActionsMenu/__tests__/__snapshots__/ActionsMenu.test.js.snap b/src/views/ActionsMenu/__tests__/__snapshots__/ActionsMenu.test.js.snap new file mode 100644 index 0000000..199a48d --- /dev/null +++ b/src/views/ActionsMenu/__tests__/__snapshots__/ActionsMenu.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the ActionsMenu snapshot 1`] = `[Function]`; diff --git a/src/views/Footer/__tests__/Footer.test.js b/src/views/Footer/__tests__/Footer.test.js new file mode 100644 index 0000000..b960928 --- /dev/null +++ b/src/views/Footer/__tests__/Footer.test.js @@ -0,0 +1,13 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; +import Footer from '../Footer'; + +describe('