diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index ce8a596..30a436c 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -30,15 +30,16 @@ const env = process.env.NODE_ENV || 'development'; webpackConfig.plugins = [ new webpack.DefinePlugin({ 'process.env': { - 'NODE_ENV': JSON.stringify(env), + NODE_ENV: JSON.stringify(env), }, - 'NODE_ENV': env, - '__DEV__': env === 'development', - '__PROD__': env === 'production', - '__TEST__': env === 'test', - '__DEBUG__': env === 'development' && !argv.no_debug, - '__COVERAGE__': !argv.watch && env === 'test', - '__BASENAME__': JSON.stringify(process.env.BASENAME || ''), + NODE_ENV: env, + __DEV__: env === 'development', + __PROD__: env === 'production', + __TEST__: env === 'test', + __DEBUG__: env === 'development' && !argv.no_debug, + __COVERAGE__: !argv.watch && env === 'test', + __BASENAME__: JSON.stringify(process.env.BASENAME || ''), + __CONTROLLER_API__: JSON.stringify('https://fake-controller.net'), }), ]; diff --git a/config/ava.config.js b/config/ava.config.js index 2b0b666..c71964f 100644 --- a/config/ava.config.js +++ b/config/ava.config.js @@ -3,6 +3,13 @@ import { argv } from 'yargs'; import fs from 'fs'; import config from '../config'; +import injectTapEventPlugin from 'react-tap-event-plugin'; + +Object.keys(config.globals).forEach(key => { + if(typeof (config.globals[key]) === 'string') { + config.globals[key] = config.globals[key].replace(/['"]+/g, ''); + }; +}); Object.assign(global, config.globals); @@ -28,3 +35,4 @@ require.extensions['.html'] = (module, filename) => { global.document = require('jsdom').jsdom(require('../src/index.html')); global.window = document.defaultView; global.navigator = window.navigator; +injectTapEventPlugin(); // for component tests using onTouchTap event diff --git a/config/index.js b/config/index.js index 0b4d2e5..7a08337 100644 --- a/config/index.js +++ b/config/index.js @@ -33,7 +33,7 @@ const config = { // ---------------------------------- // Controller Service Configuration // ---------------------------------- - controller_service : process.env.CONTROLLER_SERVICE || 'https://not.set.net', + controller_service : process.env.CONTROLLER_SERVICE, // ---------------------------------- // Compiler Configuration @@ -72,6 +72,7 @@ Edit at Your Own Risk // Environment // ------------------------------------ // N.B.: globals added here must _also_ be added to .eslintrc + config.globals = { 'process.env' : { 'NODE_ENV' : JSON.stringify(config.env), @@ -83,9 +84,7 @@ config.globals = { '__DEBUG__' : config.env === 'development' && !argv.no_debug, '__COVERAGE__' : !argv.watch && config.env === 'test', '__BASENAME__' : JSON.stringify(process.env.BASENAME || ''), - '__CONTROLLER_API__' : config.env === 'test' ? - `${config.controller_service}/api/v1` : - JSON.stringify(`${config.controller_service}/api/v1`), + '__CONTROLLER_API__' : JSON.stringify(config.controller_service || ''), }; // ------------------------------------ diff --git a/package.json b/package.json index 1761785..88e27a6 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "command": "node_modules/.bin/nyc node --harmony-proxies ./node_modules/.bin/ava", "env": { "NODE_ENV": "test", - "NODE_PATH": "src" + "NODE_PATH": "src", + "CONTROLLER_SERVICE": "https://fake-controller.net" } } }, diff --git a/src/components/GlobalNav/GlobalNav.jsx b/src/components/GlobalNav/GlobalNav.jsx index 739bbfc..f3a0594 100644 --- a/src/components/GlobalNav/GlobalNav.jsx +++ b/src/components/GlobalNav/GlobalNav.jsx @@ -2,33 +2,23 @@ import React from 'react'; import IconButton from 'material-ui/IconButton'; import { Toolbar, ToolbarGroup, ToolbarTitle, ToolbarSeparator } from 'material-ui/Toolbar'; import { Icon } from 'react-fa'; -import { palette } from 'styles/muiTheme'; -import RoleSwitcher from './RoleSwitcher'; +import { Link } from 'react-router'; +import RoleSwitcher from 'containers/RoleSwitcherContainer'; import classes from './GlobalNav.scss'; const styles = { - toolbar: { - backgroundColor: palette.primary1Color, - }, separator: { - backgroundColor: 'rgb(184, 222, 228)', - margin: '0px 12px 0px 24px', - }, - paragraph: { - paddingLeft: '2rem', - fontSize: '0.9rem', - }, - title: { - color: palette.alternateTextColor, - fontSize: '1rem', + margin: '0px 0.75rem 0px 2rem', }, }; export const GlobalNav = () => (
- + - + + + @@ -40,12 +30,13 @@ export const GlobalNav = () => ( className={classes.github} /> -

Demo Settings

+
); -GlobalNav.propTypes = {}; +GlobalNav.propTypes = { +}; export default GlobalNav; diff --git a/src/components/GlobalNav/GlobalNav.scss b/src/components/GlobalNav/GlobalNav.scss index 7758d6d..d263065 100644 --- a/src/components/GlobalNav/GlobalNav.scss +++ b/src/components/GlobalNav/GlobalNav.scss @@ -1,49 +1,15 @@ -/* GlobalNav Styles */ -.globalNav { - p { - color: rgb(255, 255, 255); - } -} +@import '~styles/base/colors'; .title { padding-left: 2rem; -} - -.separator { - background-color: rgb(184, 222, 228); + color: $alternateTextColor; } .github { font-size: 1.9rem; - color: rgb(184, 222, 228); + color: $primary3Color; &:hover { - color: rgb(255, 255, 255); - } -} - -/* RoleSwitcher Styles */ -.stack { - position: relative; - cursor: pointer; - margin-right: 30px; - - .user { - font-size: 1.2rem; - color: rgb(15, 147, 166); - line-height: 3.3rem; - margin-left: .3rem; - } - - &:hover { - .circle { - color: rgb(255, 255, 255); - } - } - - .circle { - font-size: 1.9rem; - color: rgb(184, 222, 228); - line-height: 3.3rem; + color: $alternateTextColor; } } diff --git a/src/components/GlobalNav/GlobalNav.story.js b/src/components/GlobalNav/GlobalNav.story.js index 8e0e6e2..0974313 100644 --- a/src/components/GlobalNav/GlobalNav.story.js +++ b/src/components/GlobalNav/GlobalNav.story.js @@ -3,15 +3,31 @@ import { storiesOf } from '@kadira/storybook'; import GlobalNav from './GlobalNav'; import RoleSwitcher from './RoleSwitcher'; +const users = [ + { + id: 1, + role: 'supplychainmanager', + loggedIn: true, + }, + { + id: 2, + role: 'retailstoremanager', + location: 'Austin, TX', + }, +]; + storiesOf('GlobalNav', module) .addDecorator((story) => (
{story()}
)) - .add('Default', () => ( + .add('Not Logged in', () => ( )) + .add('Logged in', () => ( + + )) .add('Role Switcher', () => ( - + )); diff --git a/src/components/GlobalNav/GlobalNav.test.js b/src/components/GlobalNav/GlobalNav.test.js index 804f89b..1e0d582 100644 --- a/src/components/GlobalNav/GlobalNav.test.js +++ b/src/components/GlobalNav/GlobalNav.test.js @@ -1,34 +1,22 @@ import test from 'ava'; import React from 'react'; import { shallow } from 'enzyme'; -// import { Icon } from 'react-fa'; import GlobalNav from './GlobalNav'; - const setup = () => { - // const spies = { - // clicky: sinon.spy(), - // }; - // const props = { - // customProp: 'Test', - // clicky: spies.clicky, - // }; + const spies = { + }; + const props = { + }; const component = shallow(); - return { component }; + return { component, props, spies }; }; test('(Component) Has expected elements.', t => { const { component } = setup(); - // t.is(component.find('Icon'), 1, - // 'contains React-FA Github Icons.'); - // t.true(component.contains(''), - // 'contains RoleSwitcher component.'); - // t.true(component.contains(''), - // 'contains Toolbar component.'); - - t.is(component.find('Icon').length, 1); - t.is(component.find('RoleSwitcher').length, 1); - t.is(component.find('Toolbar').length, 1); + t.is(component.find('Connect(RoleSwitcher)').length, 1); + t.is(component.find('Link').length, 1); + t.is(component.find('Link').first().props().to, '/'); }); diff --git a/src/components/GlobalNav/RoleItem.jsx b/src/components/GlobalNav/RoleItem.jsx deleted file mode 100644 index 5fc786b..0000000 --- a/src/components/GlobalNav/RoleItem.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import classes from './RoleItem.scss'; - -export class RoleItem extends React.Component { - getItemType = () => { - if (this.props.type === 'button') { - return ( -

- {this.props.label} -

- ); - } - else if (this.props.location) { - return ( -
-

{this.props.label}

-

{this.props.location}

-
- ); - } - - return ( -

- {this.props.label} -

- ); - } - render() { - return ( - - ); - } -} - -RoleItem.propTypes = { - selected: React.PropTypes.bool, - type: React.PropTypes.string, - location: React.PropTypes.string, - icon: React.PropTypes.string.isRequired, - label: React.PropTypes.string.isRequired, -}; - -export default RoleItem; diff --git a/src/components/GlobalNav/RoleItem.scss b/src/components/GlobalNav/RoleItem.scss deleted file mode 100644 index d5930b2..0000000 --- a/src/components/GlobalNav/RoleItem.scss +++ /dev/null @@ -1,76 +0,0 @@ -.item { - min-height: 65px; - display: flex; - width: 100%; - text-align: left; - cursor: pointer; - border: none; - border-bottom: 1px solid rgb(219, 222, 224); - background: rgb(255, 255, 255); - color: rgb(72, 85, 102); - margin: 0; - padding: 0; - padding-left: 5px; - outline: none; - - &:last-child { - border-bottom: none; - } - - &.selected { - padding-left: 0px; - border-left: 5px solid rgb(145, 195, 131); - } -} - -/* Menu Icon Styles */ -.iconContainer { - border-left: 5px solid transparent; - align-self: center; - - .icon { - font-size: 1.5rem; - color: rgb(72, 85, 102); - line-height: 2.6rem; - margin: 0px 1rem; - } - - .small { - font-size: 0.8rem; - margin-left: 1.2rem; - } -} - -/* Menu Item Text Styles */ -.textContainer { - display: flex; - align-items: center; - justify-content: center; - align-self: center; - - p { - margin: 0; - } - - .label { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - align-self: center; - } - - .label.center { - align-self: flex-start; - } - - .sublabel { - font-size: 0.7rem; - line-height: 0.7rem; - font-style: italic; - align-self: flex-end; - } - - .button { - font-weight: 500; - } -} diff --git a/src/components/GlobalNav/RoleSwitcher.jsx b/src/components/GlobalNav/RoleSwitcher.jsx deleted file mode 100644 index f3085b8..0000000 --- a/src/components/GlobalNav/RoleSwitcher.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import IconMenu from 'material-ui/IconMenu'; -import { Icon } from 'react-fa'; -import RoleItem from './RoleItem'; -import classes from './GlobalNav.scss'; - -// const styles = { -// stack: { -// position: 'relative', -// cursor: 'pointer', -// }, -// user: { -// fontSize: 30, -// color: 'rgb(15, 147, 166)', -// lineHeight: '62px', -// }, -// circle: { -// fontSize: 34, -// color: 'rgb(255, 255, 255)', -// lineHeight: '56px', -// }, -// }; - -// Props have been removed since they are not currently needed. -// To add props back in, simply change the line below to: ...RoleSwitcher = (props) => (... -export const RoleSwitcher = () => ( - // TODO: Find out what's causing runtime error. - - - - - } - > - - - - - -); -export default RoleSwitcher; diff --git a/src/components/GlobalNav/RoleSwitcher/RoleItem.jsx b/src/components/GlobalNav/RoleSwitcher/RoleItem.jsx new file mode 100644 index 0000000..f970106 --- /dev/null +++ b/src/components/GlobalNav/RoleSwitcher/RoleItem.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import classNames from 'classnames'; +import classes from './RoleItem.scss'; + +export class RoleItem extends React.PureComponent { + iconContainer = (user) => ( +
+ +
+ ) + + textContainer = (user) => ( +
+
+ {user ? user.role : 'Create New Retail Manager' } +
+ {user && user.location + ?
{user.location}
+ : '' + } +
+ ) + + handleClick = () => { + const { user, roleAction } = this.props; + + if (user && !user.loggedIn) { + roleAction(user.id); + } + else { + roleAction(); + } + } + + render() { + const user = this.props.user; + + return ( + + ); + } +} + +RoleItem.propTypes = { + user: React.PropTypes.shape({ + id: React.PropTypes.number.isRequired, + role: React.PropTypes.string.isRequired, + location: React.PropTypes.string, + loggedIn: React.PropTypes.bool, + }), + roleAction: React.PropTypes.func.isRequired, +}; + +export default RoleItem; diff --git a/src/components/GlobalNav/RoleSwitcher/RoleItem.scss b/src/components/GlobalNav/RoleSwitcher/RoleItem.scss new file mode 100644 index 0000000..7c27265 --- /dev/null +++ b/src/components/GlobalNav/RoleSwitcher/RoleItem.scss @@ -0,0 +1,69 @@ +@import '~styles/base/colors'; + +.item { + min-height: 65px; + display: flex; + width: 100%; + text-align: left; + cursor: pointer; + border: none; + border-bottom: 1px solid $borderColor; + background: rgb(255, 255, 255); + color: $primary2Color; + margin: 0; + padding: 0; + padding-left: 5px; + outline: none; + + &:last-child { + border-bottom: none; + } + + &.selected { + padding-left: 0px; + border-left: 5px solid $accent1Color; + } +} + +/* Menu Icon Styles */ +.iconContainer { + border-left: 5px solid transparent; + align-self: center; + +} + +.icon { + font-size: 1.5rem; + color: $primary2Color; + line-height: 2.6rem; + margin: 0px 1rem; +} + +.small { + font-size: 0.8rem; + margin-left: 1.2rem; +} + +/* Menu Item Text Styles */ +.textContainer { + display: flex; + align-self: center; + flex-wrap: wrap; + flex-direction: column; +} + +.light { + font-weight: 500; +} + +.sublabel { + font-size: 0.7rem; + line-height: 0.7rem; + font-style: italic; +} + +.label { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; +} diff --git a/src/components/GlobalNav/RoleSwitcher/RoleItem.test.js b/src/components/GlobalNav/RoleSwitcher/RoleItem.test.js new file mode 100644 index 0000000..c60ccd9 --- /dev/null +++ b/src/components/GlobalNav/RoleSwitcher/RoleItem.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import test from 'ava'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; +import RoleItem from './RoleItem'; + +const retailManager = ({ + id = 2, + role = 'retailstoremanager', + location = 'Austin, TX', + loggedIn = true, +} = {}) => ({ id, role, location, loggedIn }); + +const supplyManager = ({ + id = 1, + role = 'supplychainmanager', +} = {}) => ({ id, role }); + +const setup = (user) => { + const spies = { + roleAction: sinon.spy(), + }; + const props = { + user, + roleAction: spies.roleAction, + }; + const component = shallow(); + + return { component, props, spies }; +}; + +test('(Component) Supply Chain Manager, not logged in, has expected elements.', t => { + const { component } = setup(supplyManager()); + + t.true(component.is('button')); + t.false(component.hasClass('selected')); + t.is(component.find('.fa-user').length, 1); + t.is(component.find('.sublabel').length, 0); + t.is(component.find('.light').length, 0); + t.is(component.find('.small').length, 0); +}); + +test('(Component) Retail Store Manager, logged in, has expected elements.', t => { + const { component, props } = setup(retailManager()); + + t.true(component.is('button')); + t.true(component.hasClass('selected')); + t.is(component.find('.fa-user').length, 1); + t.is(component.find('.sublabel').length, 1); + t.is(component.find('.light').length, 0); + t.is(component.find('.small').length, 0); + t.regex(component.text(), new RegExp(props.user.location)); +}); + +test('(Component) No user passed in, has expected elements.', t => { + const { component } = setup(); + + t.false(component.hasClass('selected')); + t.is(component.find('.fa-plus').length, 1); + t.is(component.find('.light').length, 1); + t.is(component.find('.small').length, 1); +}); diff --git a/src/components/GlobalNav/RoleSwitcher/RoleSwitcher.jsx b/src/components/GlobalNav/RoleSwitcher/RoleSwitcher.jsx new file mode 100644 index 0000000..7f6ab80 --- /dev/null +++ b/src/components/GlobalNav/RoleSwitcher/RoleSwitcher.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import IconMenu from 'material-ui/IconMenu'; +import IconButton from 'material-ui/IconButton'; +import AccountUser from 'material-ui/svg-icons/action/account-circle'; +import { palette } from 'styles/muiTheme'; +import RoleItem from './RoleItem'; + +const iconStyles = { + width: '2rem', + height: '2rem', +}; + +const AccountButton = () => ( + + + +); + +export const RoleSwitcher = ({ users, login, createUser }) => ( + users + ? + {users.map((user, key) => ( + + ))} + + + : No Demo Session +); + +RoleSwitcher.propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.shape({ + id: React.PropTypes.number.isRequired, + role: React.PropTypes.string.isRequired, + location: React.PropTypes.string, + loggedIn: React.PropTypes.bool, + }).isRequired), + login: React.PropTypes.func.isRequired, + createUser: React.PropTypes.func.isRequired, +}; + +export default RoleSwitcher; diff --git a/src/components/GlobalNav/RoleSwitcher/RoleSwitcher.scss b/src/components/GlobalNav/RoleSwitcher/RoleSwitcher.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/GlobalNav/RoleSwitcher/index.js b/src/components/GlobalNav/RoleSwitcher/index.js new file mode 100644 index 0000000..0260bf4 --- /dev/null +++ b/src/components/GlobalNav/RoleSwitcher/index.js @@ -0,0 +1,3 @@ +import RoleSwitcher from './RoleSwitcher'; + +export default RoleSwitcher; diff --git a/src/containers/RoleSwitcherContainer.js b/src/containers/RoleSwitcherContainer.js new file mode 100644 index 0000000..a09e332 --- /dev/null +++ b/src/containers/RoleSwitcherContainer.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { login, createUser } from 'modules/demos'; +import RoleSwitcher from 'components/GlobalNav/RoleSwitcher'; + +const mapActionCreators = { + login, + createUser, +}; + +const mapStateToProps = (state) => ({ + users: state.demoSession.users, +}); + +export default connect(mapStateToProps, mapActionCreators)(RoleSwitcher); diff --git a/src/modules/demos.js b/src/modules/demos.js index efec4c7..084f7cd 100644 --- a/src/modules/demos.js +++ b/src/modules/demos.js @@ -1,44 +1,88 @@ +import { call, take, put, select } from 'redux-saga/effects'; +import api from 'services'; + // ------------------------------------ // Constants // ------------------------------------ -export const RECEIVE_DEMO_SUCCESS = 'demos/RECEIVE_DEMO_SUCCESS'; +export const GET_DEMO_SESSION = 'demos/GET_DEMO_SESSION'; +export const GET_DEMO_SUCCESS = 'demos/GET_DEMO_SUCCESS'; +export const LOGIN = 'demos/LOGIN'; export const LOGIN_SUCCESS = 'demos/LOGIN_SUCCESS'; +export const demoSelector = state => state.demoSession; + // ------------------------------------ // Actions // ------------------------------------ -export const receiveDemoSuccess = (value) => ({ - type: RECEIVE_DEMO_SUCCESS, - payload: value, +export const getDemoSession = (guid) => ({ + type: GET_DEMO_SESSION, + guid, +}); + +export const getDemoSuccess = (payload) => ({ + type: GET_DEMO_SUCCESS, + demo: payload.demo, + retailers: payload.retailers, +}); + +// TODO finish this v +export const createUser = () => ({ + type: 'CREATE_USER', +}); + +export const login = (userid) => ({ + type: LOGIN, + userid, }); -export const loginSuccess = (value) => ({ +export const loginSuccess = ({ token, userid }) => ({ type: LOGIN_SUCCESS, - payload: value, + token, + userid, }); export const actions = { - receiveDemoSuccess, + getDemoSuccess, loginSuccess, }; // ------------------------------------ // Action Handlers // ------------------------------------ +const matchUserToLocation = (userid, retailers) => { + const location = retailers.find((retailer) => retailer.managerId === userid).address; + return `${location.city}, ${location.state}`; +}; + +const mapUserData = (users, retailers) => + users.map(user => { + const isSupplyManager = user.roles[0].name.includes('supply'); + const userMapping = { + id: user.id, + role: isSupplyManager ? 'Supply Chain Manager' : 'Retail Store Manager', + }; + + if (!isSupplyManager) { + userMapping.location = matchUserToLocation(user.id, retailers); + } + + return userMapping; + }); + const ACTION_HANDLERS = { - [RECEIVE_DEMO_SUCCESS]: (state, action) => ({ + [GET_DEMO_SUCCESS]: (state, { demo, retailers }) => ({ ...state, - name: action.payload.name, - guid: action.payload.guid, - users: [{ - id: action.payload.users[0].id, - username: action.payload.users[0].username, - type: action.payload.users[0].roles[0].name, - }], + name: demo.name, + guid: demo.guid, + users: mapUserData(demo.users, retailers), }), - [LOGIN_SUCCESS]: (state, action) => ({ + [LOGIN_SUCCESS]: (state, { token, userid }) => ({ ...state, - token: action.payload, + token: token.token, + users: state.users.map(user => ({ + ...user, + loggedIn: user.id === userid, + })), }), }; @@ -57,7 +101,45 @@ export default demosReducer; // ------------------------------------ // Sagas // ------------------------------------ -export const demoSelector = state => state.demoSession; +export function *watchGetDemoSession() { + while (true) { + const { guid } = yield take(GET_DEMO_SESSION); + let demoState = yield select(demoSelector); + + if (demoState.guid !== guid) { + try { + const [demo, retailers] = yield [ + call(api.getDemo, guid), + call(api.getRetailers, guid), + ]; + yield put(getDemoSuccess({ demo, retailers })); + demoState = yield select(demoSelector); + yield put(login(demoState.users[0].id)); + } + catch (error) { + console.log('Get Demo Failure: ', error); + // yield put(getDemoFailure(error)); + } + } + } +} + +export function *watchLogin() { + while (true) { + const { userid } = yield take(LOGIN); + const demoState = yield select(demoSelector); + try { + const token = yield call(api.login, userid, demoState.guid); + yield put(loginSuccess({ token, userid })); + } + catch (error) { + console.log('Login Failure: ', error); + // yield put(loginFailure(error)); + } + } +} export const sagas = [ + watchGetDemoSession, + watchLogin, ]; diff --git a/src/modules/demos.test.js b/src/modules/demos.test.js index 659ebe1..3c00585 100644 --- a/src/modules/demos.test.js +++ b/src/modules/demos.test.js @@ -1,38 +1,70 @@ import test from 'ava'; import { reducerTest, actionTest } from 'redux-ava'; +import { call, take, select, put } from 'redux-saga/effects'; +import api from 'services'; +import mockApi from 'services/mockApi'; import { - RECEIVE_DEMO_SUCCESS, + GET_DEMO_SESSION, + GET_DEMO_SUCCESS, LOGIN_SUCCESS, - receiveDemoSuccess, + LOGIN, + getDemoSession, + getDemoSuccess, + login, loginSuccess, demosReducer, demoSelector, + watchGetDemoSession, + watchLogin, } from './demos'; + test('(Selector) returns the slice of state for demos.', t => { t.deepEqual(demoSelector({ demoSession: { id: '123' } }), { id: '123' }); }); -test('(Constant) RECEIVE_DEMO_SUCCESS === "demos/RECEIVE_DEMO_SUCCESS"', t => { - t.is(RECEIVE_DEMO_SUCCESS, 'demos/RECEIVE_DEMO_SUCCESS'); +test('(Constant) GET_DEMO_SESSION === "demos/GET_DEMO_SESSION"', t => { + t.is(GET_DEMO_SESSION, 'demos/GET_DEMO_SESSION'); +}); + +test('(Constant) GET_DEMO_SUCCESS === "demos/GET_DEMO_SUCCESS"', t => { + t.is(GET_DEMO_SUCCESS, 'demos/GET_DEMO_SUCCESS'); +}); + +test('(Constant) LOGIN === "demos/LOGIN"', t => { + t.is(LOGIN, 'demos/LOGIN'); }); test('(Constant) LOGIN_SUCCESS === "demos/LOGIN_SUCCESS"', t => { t.is(LOGIN_SUCCESS, 'demos/LOGIN_SUCCESS'); }); -test('(Action) receiveDemoSuccess', +test('(Action) getDemoSession', actionTest( - receiveDemoSuccess, - { name: 'test' }, - { type: RECEIVE_DEMO_SUCCESS, payload: { name: 'test' } }) + getDemoSession, + '1234', + { type: GET_DEMO_SESSION, guid: '1234' }) + ); + +test('(Action) getDemoSuccess', + actionTest( + getDemoSuccess, + { demo: 'test', retailers: [1, 2, 3] }, + { type: GET_DEMO_SUCCESS, demo: 'test', retailers: [1, 2, 3] }) + ); + +test('(Action) login', + actionTest( + login, + '1234', + { type: LOGIN, userid: '1234' }) ); test('(Action) loginSuccess', actionTest( loginSuccess, - { name: 'test' }, - { type: LOGIN_SUCCESS, payload: { name: 'test' } }) + { token: { token: 'token' }, userid: 'userid' }, + { type: LOGIN_SUCCESS, token: { token: 'token' }, userid: 'userid' }) ); test('(Reducer) initializes with empty state', t => { @@ -46,35 +78,141 @@ test('(Reducer) return previous state when no action is matched', reducerTest( {}, )); -test('(Reducer) stores token on loginSuccess', reducerTest( +test('(Reducer) doesnt try to handle getDemoSession Saga', reducerTest( demosReducer, {}, - loginSuccess('login-token'), - { token: 'login-token' }, + getDemoSession('1234'), + {}, )); -const demoApiResponse = () => ({ - name: 'demo', - guid: 'guid', - id: 'demoid', - users: [{ - id: 'userid', - username: 'johndoe', - roles: [{ name: 'supplychainmanager' }], - }], -}); +test('(Reducer) doesnt try to handle login Saga', reducerTest( + demosReducer, + {}, + login('1234'), + {}, +)); + +test('(Reducer) stores token on loginSuccess, and changes logged in user', reducerTest( + demosReducer, + { + name: 'demo', + guid: 'guid', + token: 'old-token', + users: [{ + id: 100, + role: 'Supply Chain Manager', + loggedIn: true, + }, { + id: 200, + role: 'Retail Store Manager', + location: 'Austin, Texas', + loggedIn: false, + }], + }, + loginSuccess({ token: mockApi.login('new-token'), userid: 200 }), + { + name: 'demo', + guid: 'guid', + token: 'new-token', + users: [{ + id: 100, + role: 'Supply Chain Manager', + loggedIn: false, + }, { + id: 200, + role: 'Retail Store Manager', + location: 'Austin, Texas', + loggedIn: true, + }], + }, +)); -test('(Reducer) adds demo session to state on receiveDemoSuccess', reducerTest( +test('(Reducer) adds demo session to state on getDemoSuccess', reducerTest( demosReducer, {}, - receiveDemoSuccess(demoApiResponse()), + getDemoSuccess({ + demo: (() => { + const demo = mockApi.getDemo({ name: 'demo', guid: 'guid' }); + demo.users.push(mockApi.getUser(200)); + return demo; + })(), + retailers: [ + mockApi.getRetailer({ + city: 'Austin', + state: 'Texas', + managerId: 200, + }), + ], + }), { name: 'demo', guid: 'guid', users: [{ - id: 'userid', - username: 'johndoe', - type: 'supplychainmanager', + id: 100, + role: 'Supply Chain Manager', + }, { + id: 200, + role: 'Retail Store Manager', + location: 'Austin, Texas', }], }, )); + +test('(Saga) watchGetDemoSession - new Guid, API Success', t => { + const saga = watchGetDemoSession(); + + t.deepEqual(saga.next().value, take(GET_DEMO_SESSION)); + + const newGuid = 'Another Guid'; + const action = getDemoSession(newGuid); + t.deepEqual(saga.next(action).value, select(demoSelector)); + + // Saga should see that our guid doesn't match, so fetches new demo and logs in. + + t.truthy(action.guid); + let demoState = demosReducer({}, { type: '@@@@@' }); + t.deepEqual( + saga.next(demoState).value, + [call(api.getDemo, action.guid), call(api.getRetailers, action.guid)] + ); + + const demoPayload = mockApi.getDemo({ guid: newGuid }); + const retailersPayload = [mockApi.getRetailer()]; + t.deepEqual( + saga.next([demoPayload, retailersPayload]).value, + put(getDemoSuccess({ demo: demoPayload, retailers: retailersPayload })) + ); + t.deepEqual(saga.next().value, select(demoSelector)); + + demoState = demosReducer(demoState, getDemoSuccess({ demo: demoPayload, retailers: retailersPayload })); + t.deepEqual(saga.next(demoState).value, put(login(demoState.users[0].id))); + + // Saga loops back to beginning + t.deepEqual(saga.next().value, take(GET_DEMO_SESSION)); +}); + +test.todo('(Saga) watchGetDemoSession - Write error handling logic'); + + +test('(Saga) watchLogin - Not logged in, API Success', t => { + const saga = watchLogin(); + const demo = mockApi.getDemo(); + demo.users.push(mockApi.getUser(200)); + const retailers = [mockApi.getRetailer({ managerId: 200 })]; + + const action = login(100); + t.deepEqual(saga.next().value, take(LOGIN)); + + const demoState = demosReducer({}, getDemoSuccess({ demo, retailers })); + t.deepEqual(saga.next(action).value, select(demoSelector)); + + t.truthy(action.userid); + t.truthy(demoState.guid); + const token = mockApi.login(); + t.deepEqual(saga.next(demoState).value, call(api.login, action.userid, demoState.guid)); + + t.deepEqual(saga.next(token).value, put(loginSuccess({ token, userid: action.userid }))); + + // Saga loops back to beginning + t.deepEqual(saga.next().value, take(LOGIN)); +}); diff --git a/src/routes/CreateDemo/modules/CreateDemo.js b/src/routes/CreateDemo/modules/CreateDemo.js index 3c3e755..9dee082 100644 --- a/src/routes/CreateDemo/modules/CreateDemo.js +++ b/src/routes/CreateDemo/modules/CreateDemo.js @@ -1,9 +1,7 @@ -import { call, take, put, select } from 'redux-saga/effects'; +import { call, take, put } from 'redux-saga/effects'; import { push } from 'react-router-redux'; import api from 'services'; -import { receiveDemoSuccess, demoSelector } from 'modules/demos'; - export const createDemoSelector = state => state.createDemo; // ------------------------------------ // Constants @@ -15,9 +13,13 @@ export const CREATE_DEMO_FAILURE = 'CreateDemo/CREATE_DEMO_FAILURE'; // ------------------------------------ // Actions // ------------------------------------ -export const createDemo = (value) => ({ +export const createDemo = ({ + name = `Test Demo ${Date.now()}`, + email, +} = {}) => ({ type: CREATE_DEMO, - payload: value, + name, + email, }); export const createDemoFailure = (value) => ({ @@ -56,13 +58,11 @@ export default createDemoReducer; // ------------------------------------ export function *watchCreateDemo() { while (true) { - const { payload } = yield take(CREATE_DEMO); + const action = yield take(CREATE_DEMO); try { - const demoSession = yield call(api.createDemo, payload.name, payload.email); - yield put(receiveDemoSuccess(demoSession)); - const demoState = yield select(demoSelector); - yield put(push(`/dashboard/${demoState.guid}`)); + const demoSession = yield call(api.createDemo, action.name, action.email); + yield put(push(`/dashboard/${demoSession.guid}`)); } catch (error) { yield put(createDemoFailure(error)); diff --git a/src/routes/CreateDemo/modules/CreateDemo.test.js b/src/routes/CreateDemo/modules/CreateDemo.test.js index 7a0ea3a..adb9dd5 100644 --- a/src/routes/CreateDemo/modules/CreateDemo.test.js +++ b/src/routes/CreateDemo/modules/CreateDemo.test.js @@ -1,9 +1,9 @@ import test from 'ava'; import { reducerTest, actionTest } from 'redux-ava'; -import { call, take, select, put } from 'redux-saga/effects'; -import { receiveDemoSuccess, demoSelector } from 'modules/demos'; +import { call, take, put } from 'redux-saga/effects'; import { push } from 'react-router-redux'; import api from 'services'; +import mockApi from 'services/mockApi'; import { CREATE_DEMO, CREATE_DEMO_FAILURE, @@ -11,10 +11,11 @@ import { createDemoFailure, createDemoReducer, watchCreateDemo, + createDemoSelector, } from './CreateDemo'; test('(Selector) returns the slice of state for createDemo.', t => { - t.deepEqual(demoSelector({ demoSession: { id: '123' } }), { id: '123' }); + t.deepEqual(createDemoSelector({ createDemo: { stuff: '123' } }), { stuff: '123' }); }); test('(Constant) CREATE_DEMO === "CreateDemo/CREATE_DEMO"', t => { @@ -29,7 +30,14 @@ test('(Action) createDemo', actionTest( createDemo, { name: 'test' }, - { type: CREATE_DEMO, payload: { name: 'test' } }) + { type: CREATE_DEMO, name: 'test', email: undefined }), + ); + +test('(Action) createDemo - with email', + actionTest( + createDemo, + { name: 'test', email: 'name@email.com' }, + { type: CREATE_DEMO, name: 'test', email: 'name@email.com' }), ); test('(Action) createDemoFailure', @@ -67,21 +75,14 @@ test('(Reducer) doesnt try to handle saga', reducerTest( test('(Saga) watchCreateDemo - API Success', t => { const saga = watchCreateDemo(); - const payload = { name: 'test demo', email: 'name@email.com' }; - const createDemoAction = createDemo(payload); - const demoSession = { mockResponse: 'blah blah' }; - const demoState = { guid: 1234 }; + const action = createDemo(); + + t.deepEqual(saga.next().value, take(CREATE_DEMO)); + t.deepEqual(saga.next(action).value, call(api.createDemo, action.name, action.email)); + + const response = mockApi.getDemo(); + t.deepEqual(saga.next(response).value, put(push(`/dashboard/${response.guid}`))); - t.deepEqual(saga.next().value, take(CREATE_DEMO), - 'listens for CREATE_DEMO action.'); - t.deepEqual(saga.next(createDemoAction).value, call(api.createDemo, payload.name, payload.email), - 'calls api with action payload as params.'); - t.deepEqual(saga.next(demoSession).value, put(receiveDemoSuccess(demoSession)), - 'dispatches receiveDemoSuccess action.'); - t.deepEqual(saga.next().value, select(demoSelector), - 'gets the updated state.'); - t.deepEqual(saga.next(demoState).value, put(push(`/dashboard/${demoState.guid}`)), - 'dispatches route change to dashboard'); t.deepEqual(saga.next().value, take(CREATE_DEMO), 'saga resets, and begins listening for CREATE_DEMO again.'); }); @@ -89,14 +90,12 @@ test('(Saga) watchCreateDemo - API Success', t => { test.todo('Build a meaningful action around api failure.'); test('(Saga) watchCreateDemo - API Failure', t => { const saga = watchCreateDemo(); - const payload = { name: 'test demo', email: 'name@email.com' }; - const createDemoAction = createDemo(payload); - const error = { message: 'bad email' }; + const action = createDemo(); - t.deepEqual(saga.next().value, take(CREATE_DEMO), - 'listens for CREATE_DEMO action.'); - t.deepEqual(saga.next(createDemoAction).value, call(api.createDemo, payload.name, payload.email), - 'calls api with action payload as params.'); + t.deepEqual(saga.next().value, take(CREATE_DEMO)); + t.deepEqual(saga.next(action).value, call(api.createDemo, action.name, action.email)); + + const error = { message: 'bad email' }; t.deepEqual(saga.throw(error).value, put(createDemoFailure(error)), 'dispatches createDemoFailure if api call fails.'); t.deepEqual(saga.next().value, take(CREATE_DEMO), diff --git a/src/routes/Dashboard/components/Dashboard.jsx b/src/routes/Dashboard/components/Dashboard.jsx index be627cc..9a3111d 100644 --- a/src/routes/Dashboard/components/Dashboard.jsx +++ b/src/routes/Dashboard/components/Dashboard.jsx @@ -2,26 +2,24 @@ import React from 'react'; import classes from './Dashboard.scss'; export default class Dashboard extends React.PureComponent { - constructor(props) { - super(props); - props.getAdminData(props.params.guid); - } - render() { return (

Dashboard - Yay, you created a demo!

Demo Name: {this.props.demoName || 'loading...'}

-
{this.props.dbdata ? JSON.stringify(this.props.dbdata, null, 2) : 'loading...'}
+
+          {
+            Object.keys(this.props.dbdata).length !== 0
+            ? JSON.stringify(this.props.dbdata, null, 2)
+            : 'loading...'
+          }
+        
); } } - Dashboard.propTypes = { - demoName: React.PropTypes.string.isRequired, + demoName: React.PropTypes.string, dbdata: React.PropTypes.object.isRequired, - getAdminData: React.PropTypes.func.isRequired, - params: React.PropTypes.object.isRequired, -}; \ No newline at end of file +}; diff --git a/src/routes/Dashboard/components/Dashboard.test.js b/src/routes/Dashboard/components/Dashboard.test.js index 7d27c55..2758a5c 100644 --- a/src/routes/Dashboard/components/Dashboard.test.js +++ b/src/routes/Dashboard/components/Dashboard.test.js @@ -1,22 +1,15 @@ import test from 'ava'; import React from 'react'; -import sinon from 'sinon'; -import { bindActionCreators } from 'redux'; import { shallow } from 'enzyme'; import Dashboard from './Dashboard'; const setup = () => { const spies = { - getAdminData: sinon.spy(), - dispatch: sinon.spy(), }; const props = { demoName: 'Test Demo', dbdata: { fakeData: 'fake stuff' }, params: { guid: '1234' }, - ...bindActionCreators({ - getAdminData: spies.getAdminData, - }, spies.dispatch), }; const component = shallow(); @@ -27,16 +20,6 @@ test.todo('write tests for dashboard elements once complete.'); test('(Component) Renders with expected elements', t => { const { props, component } = setup(); - t.true(component.is('div'), - 'is wrapped by a div'); t.regex(component.find('p').first().text(), new RegExp(props.demoName, 'g'), 'renders demo name from props'); }); - -test('(Component) Works as expected.', t => { - const { spies } = setup(); - - t.true(spies.getAdminData.calledOnce, - 'getAdminData is called on creation'); - t.true(spies.dispatch.calledOnce); -}); diff --git a/src/routes/Dashboard/containers/DashboardContainer.js b/src/routes/Dashboard/containers/DashboardContainer.js index 5fc50c1..0ab9821 100644 --- a/src/routes/Dashboard/containers/DashboardContainer.js +++ b/src/routes/Dashboard/containers/DashboardContainer.js @@ -1,9 +1,7 @@ import { connect } from 'react-redux'; -import { getAdminData } from '../modules/Dashboard'; import Dashboard from '../components/Dashboard'; const mapActionCreators = { - getAdminData, }; const mapStateToProps = (state) => ({ diff --git a/src/routes/Dashboard/index.js b/src/routes/Dashboard/index.js index 03b9237..16a8e7c 100644 --- a/src/routes/Dashboard/index.js +++ b/src/routes/Dashboard/index.js @@ -1,5 +1,6 @@ import { injectReducer } from 'store/reducers'; import { injectSagas } from 'store/sagas'; +import { getDemoSession } from 'modules/demos'; export default (store) => ({ path: 'dashboard/:guid', @@ -12,7 +13,11 @@ export default (store) => ({ injectReducer(store, { key: 'dashboard', reducer }); injectSagas(store, { key: 'dashboard', sagas }); + store.dispatch(getDemoSession(nextState.params.guid)); cb(null, Dashboard); }, 'dashboard'); }, + onEnter: (nextState) => { + store.dispatch(getDemoSession(nextState.params.guid)); + }, }); diff --git a/src/routes/Dashboard/index.test.js b/src/routes/Dashboard/index.test.js index 5d2d2d0..abd3f73 100644 --- a/src/routes/Dashboard/index.test.js +++ b/src/routes/Dashboard/index.test.js @@ -8,3 +8,8 @@ test('(Route) should return a route config object', t => { test('(Route) Config should contain path "dashboard/:guid"', t => { t.is(DashboardRoute({}).path, 'dashboard/:guid'); }); + +test('(Route) Config should contain logic to dispatch getDemo action', t => { + t.pass(); + // t.is(DashboardRoute({}).onEnter, Test Function Somehow?); +}); diff --git a/src/routes/Dashboard/modules/Dashboard.js b/src/routes/Dashboard/modules/Dashboard.js index 1aba537..c574d12 100644 --- a/src/routes/Dashboard/modules/Dashboard.js +++ b/src/routes/Dashboard/modules/Dashboard.js @@ -1,6 +1,8 @@ import { call, take, put, select } from 'redux-saga/effects'; import api from 'services'; -import { loginSuccess, receiveDemoSuccess, demoSelector } from 'modules/demos'; +import { demoSelector } from 'modules/demos'; + +export const dashboardSelector = state => state.dashboard; // ------------------------------------ // Constants @@ -52,34 +54,10 @@ export default dashboardReducer; // Sagas // ------------------------------------ -// This is set up in `../index.js` as the key in `injectSagas(store, { key: 'dashboard', sagas });` -export const dashboardSelector = state => state.dashboard; - export function *watchGetAdminData() { while (true) { - const { guid } = yield take(GET_ADMIN_DATA); - let demoState = yield select(demoSelector); - if (demoState.guid !== guid) { - try { - const demoSession = yield call(api.getDemo, guid); - yield put(receiveDemoSuccess(demoSession)); - demoState = yield select(demoSelector); - } - catch (error) { - // console.log(error); - // yield put(receiveDemoFailure(error)); - } - - try { - const { token } = yield call(api.login, demoState.users[0].id, demoState.guid); - yield put(loginSuccess(token)); - demoState = yield select(demoSelector); - } - catch (error) { - // console.log(error); - // yield put(loginFailure(error)); - } - } + yield take(GET_ADMIN_DATA); + const demoState = yield select(demoSelector); try { const adminData = yield call(api.getAdminData, demoState.token); diff --git a/src/routes/Dashboard/modules/Dashboard.test.js b/src/routes/Dashboard/modules/Dashboard.test.js index 43bb77b..403ab5e 100644 --- a/src/routes/Dashboard/modules/Dashboard.test.js +++ b/src/routes/Dashboard/modules/Dashboard.test.js @@ -1,7 +1,7 @@ import test from 'ava'; import { reducerTest, actionTest } from 'redux-ava'; import { call, take, select, put } from 'redux-saga/effects'; -import { demosReducer, loginSuccess, receiveDemoSuccess, demoSelector } from 'modules/demos'; +import { demosReducer, loginSuccess, getDemoSuccess, demoSelector } from 'modules/demos'; import api from 'services'; import mockApi from 'services/mockApi'; import { @@ -66,62 +66,29 @@ test('(Reducer) doesnt try to handle saga', reducerTest( { mock: 'mock' }, )); -test('(Saga) watchGetAdminData - Not logged in, API Success', t => { +test('(Saga) watchGetAdminData - API Success', t => { const saga = watchGetAdminData(); - let demoState = demosReducer({}, receiveDemoSuccess(mockApi.getDemo())); + // Setup initial state + const demo = mockApi.getDemo(); + demo.users.push(mockApi.getUser(200)); + const retailers = [mockApi.getRetailer({ managerId: 200 })]; + let demoState = demosReducer({}, getDemoSuccess({ demo, retailers })); + const token = mockApi.login(); + demoState = demosReducer(demoState, loginSuccess({ token, userid: demoState.users[0].id })); const newGuid = 'Another Guid'; const action = getAdminData(newGuid); t.deepEqual(saga.next().value, take(GET_ADMIN_DATA)); - t.deepEqual(saga.next(action).value, select(demoSelector)); - - // Saga should see that our guid doesn't match, so fetches new demo and logs in. - - t.truthy(action.guid); - t.deepEqual(saga.next(demoState).value, call(api.getDemo, action.guid)); - - const demoPayload = mockApi.getDemo(newGuid); - t.deepEqual(saga.next(demoPayload).value, put(receiveDemoSuccess(demoPayload))); - t.deepEqual(saga.next().value, select(demoSelector)); - demoState = demosReducer(demoState, receiveDemoSuccess(demoPayload)); - - t.truthy(demoState.users[0].id); - t.truthy(demoState.guid); - t.deepEqual(saga.next(demoState).value, call(api.login, demoState.users[0].id, demoState.guid)); - const loginPayload = mockApi.login(); - t.deepEqual(saga.next(loginPayload).value, put(loginSuccess(loginPayload.token))); - t.deepEqual(saga.next().value, select(demoSelector)); - demoState = demosReducer(demoState, loginSuccess(loginPayload.token)); - - t.truthy(demoState.token); - t.deepEqual(saga.next(demoState).value, call(api.getAdminData, demoState.token)); - const adminPayload = mockApi.getAdminData(); - t.deepEqual(saga.next(adminPayload).value, put(adminDataReceived(adminPayload))); - - // Saga loops back to beginning - t.deepEqual(saga.next().value, take(GET_ADMIN_DATA)); -}); -test('(Saga) watchGetAdminData - Already logged in, API Success', t => { - const saga = watchGetAdminData(); - let demoState = demosReducer({}, receiveDemoSuccess(mockApi.getDemo())); - demoState = demosReducer(demoState, loginSuccess(mockApi.login().token)); - - const action = getAdminData(demoState.guid); - t.deepEqual(saga.next().value, take(GET_ADMIN_DATA)); t.deepEqual(saga.next(action).value, select(demoSelector)); - // Saga should see that our guid already matches here, so goes straight to api.getAdminData - + const adminPayload = mockApi.getAdminData(); t.truthy(demoState.token); t.deepEqual(saga.next(demoState).value, call(api.getAdminData, demoState.token)); - - const adminPayload = mockApi.getAdminData(); t.deepEqual(saga.next(adminPayload).value, put(adminDataReceived(adminPayload))); // Saga loops back to beginning t.deepEqual(saga.next().value, take(GET_ADMIN_DATA)); }); - -test.todo('Build a meaningful action around api failure.'); +test.todo('(Saga) watchGetAdminData - Write failure logic.'); diff --git a/src/routes/Home/components/LandingPage/LogisticsWizard/LogisticsWizard.jsx b/src/routes/Home/components/LandingPage/LogisticsWizard/LogisticsWizard.jsx index 77ce10c..5c484df 100644 --- a/src/routes/Home/components/LandingPage/LogisticsWizard/LogisticsWizard.jsx +++ b/src/routes/Home/components/LandingPage/LogisticsWizard/LogisticsWizard.jsx @@ -9,8 +9,8 @@ export const LogisticsWizard = () => (

Logistics Wizard

A cognitive logistics solution that analyzes real-time data, provides - intelligent recommendations, and presents your employees with a beautiful - monitoring dashboard to help lead your supply chain management system into the future. + intelligent recommendations, and presents your employees with a beautiful + monitoring dashboard to help lead your supply chain management system into the future.

({ path: '/', indexRoute: Home, childRoutes: [ - CreateDemoRoute(store), - DashboardRoute(store), + { component: CoreLayout, + childRoutes: [ + CreateDemoRoute(store), + DashboardRoute(store), + ], + }, ], }); diff --git a/src/services/index.js b/src/services/index.js index 05e0b3b..01f6004 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,7 +1,7 @@ -export const CONTROLLER_URL = __CONTROLLER_API__; +export const controllerApi = `${__CONTROLLER_API__}/api/v1`; export const callApi = (endpoint, { - apiUrl = CONTROLLER_URL, + apiUrl = controllerApi, headers = { 'Content-Type': 'application/json' }, method = 'GET', body, @@ -9,7 +9,7 @@ export const callApi = (endpoint, { fetch(`${apiUrl}/${endpoint}`, { headers, method, - body: body ? JSON.stringify(body) : undefined, + body: JSON.stringify(body), }) .then(response => response.json().then(json => ({ json, response }))) .then(({ json, response }) => { @@ -24,7 +24,7 @@ export const createDemo = (name, email) => body: { name, email }, }); -export const getDemo = (guid) => callApi(`demos/${guid}`); +export const getDemo = guid => callApi(`demos/${guid}`); export const login = (id, guid) => callApi(`demos/${guid}/login`, { @@ -32,6 +32,8 @@ export const login = (id, guid) => body: { userId: id }, }); +export const getRetailers = guid => callApi(`demos/${guid}/retailers`); + export const getAdminData = token => callApi('admin', { headers: { Authorization: `Bearer ${token}` } }); @@ -39,6 +41,7 @@ export const api = { createDemo, getDemo, login, + getRetailers, getAdminData, }; diff --git a/src/services/index.test.js b/src/services/index.test.js index f5fa4f6..3aaa619 100644 --- a/src/services/index.test.js +++ b/src/services/index.test.js @@ -1,12 +1,13 @@ import test from 'ava'; import nock from 'nock'; import { + controllerApi, createDemo, getDemo, login, + getRetailers, getAdminData, - CONTROLLER_URL, -} from './'; +} from '../services'; test('(API) createDemo', function *(t) { t.plan(2); @@ -14,7 +15,7 @@ test('(API) createDemo', function *(t) { const endpoint = '/demos'; const success = { id: 123, name: 'demo' }; - nock(CONTROLLER_URL) + nock(controllerApi) .post(endpoint, { name: 'demo', email: 'email@company.com', @@ -25,7 +26,7 @@ test('(API) createDemo', function *(t) { t.deepEqual(response, success); const fail = { message: 'Invalid email address' }; - nock(CONTROLLER_URL) + nock(controllerApi) .post(endpoint, { name: 'demo', email: 'bademail', @@ -47,7 +48,7 @@ test('(API) getDemo', function *(t) { const endpoint = `/demos/${guid}`; const success = { guid, id: 123, name: 'demo' }; - nock(CONTROLLER_URL) + nock(controllerApi) .get(endpoint) .reply(200, success); @@ -55,7 +56,7 @@ test('(API) getDemo', function *(t) { t.deepEqual(response, success); const fail = { message: 'Demo does not exist' }; - nock(CONTROLLER_URL) + nock(controllerApi) .get('/demos/1111') .reply(404, fail); @@ -76,7 +77,7 @@ test('(API) login', function *(t) { const endpoint = `/demos/${guid}/login`; const success = { token: 'logintoken' }; - nock(CONTROLLER_URL) + nock(controllerApi) .post(endpoint, { userId }) .reply(200, success); @@ -84,7 +85,7 @@ test('(API) login', function *(t) { t.deepEqual(response, success); const fail = { message: 'Demo or user does not exist' }; - nock(CONTROLLER_URL) + nock(controllerApi) .post(endpoint, { userId }) .reply(404, fail); @@ -97,6 +98,34 @@ test('(API) login', function *(t) { } }); +test('(API) getRetailers', function *(t) { + t.plan(2); + + const guid = '1234'; + const endpoint = `/demos/${guid}/retailers`; + const success = [1, 2, 3]; + + nock(controllerApi) + .get(endpoint) + .reply(200, success); + + const response = yield getRetailers(guid); + t.deepEqual(response, success); + + const fail = { message: 'Demo does not exist' }; + nock(controllerApi) + .get(endpoint) + .reply(404, fail); + + try { + yield getRetailers(guid); + } + catch (error) { + t.deepEqual(error, fail, + 'promise should fail with api message as the error'); + } +}); + test('(API) getAdminData', function *(t) { t.plan(2); @@ -104,7 +133,7 @@ test('(API) getAdminData', function *(t) { const endpoint = '/admin'; const success = { mockData: 'blahblah' }; - nock(CONTROLLER_URL) + nock(controllerApi) .get(endpoint) .reply(200, success); @@ -112,7 +141,7 @@ test('(API) getAdminData', function *(t) { t.deepEqual(response, success); const fail = { message: 'Unauthorized' }; - nock(CONTROLLER_URL) + nock(controllerApi) .get(endpoint) .reply(401, fail); diff --git a/src/services/mockApi.js b/src/services/mockApi.js index 6277484..8adc96a 100644 --- a/src/services/mockApi.js +++ b/src/services/mockApi.js @@ -1,30 +1,49 @@ import faker from 'faker'; +export const getUser = (id, role = 'retailstoremanager') => ({ + username: faker.name.firstName(), + email: faker.internet.email(), + created: null, + id, + demoId: faker.random.number(), + roles: [{ + id: 1, + name: role, + description: null, + created: faker.date.recent(), + modified: faker.date.recent(), + }], +}); + export const getDemo = ({ + name = faker.fake('{{lorem.word}} {{random.number}}'), guid = faker.random.uuid(), } = {}) => ({ createdAt: faker.date.recent(), - name: faker.fake('{{lorem.word}} {{random.number}}'), + name, guid, id: faker.random.number(), - users: [{ - username: faker.name.firstName(), - email: faker.internet.email(), - created: null, - id: faker.random.number(), - demoId: faker.random.number(), - roles: [{ - id: 1, - name: 'supplychainmanager', - description: null, - created: faker.date.recent(), - modified: faker.date.recent(), - }], - }], + users: [ + getUser(100, 'supplychainmanager'), + ], }); -export const login = () => ({ - token: faker.random.uuid(), +export const login = (token = faker.random.uuid()) => ({ token }); + +export const getRetailer = ({ + managerId = null, + state = faker.address.state(), + city = faker.address.city(), +} = {}) => ({ + managerId, + address: { + state, + city, + latitude: faker.address.latitude(), + country: faker.address.country(), + longitude: faker.address.longitude(), + }, + id: 405, }); export const getAdminData = () => ({ @@ -58,22 +77,14 @@ export const getAdminData = () => ({ toId: 405, updatedAt: null, }], - retailers: [{ - managerId: null, - address: { - state: faker.address.state(), - city: faker.address.city(), - latitude: faker.address.latitude(), - country: faker.address.country(), - longitude: faker.address.longitude(), - }, - id: 405, - }], + retailers: [getRetailer()], }); export const mockApi = { + getUser, getDemo, login, + getRetailer, getAdminData, }; diff --git a/src/styles/base/_colors.scss b/src/styles/base/_colors.scss index 7b283d0..7e3e06e 100644 --- a/src/styles/base/_colors.scss +++ b/src/styles/base/_colors.scss @@ -1,12 +1,15 @@ -$primary1Color: #0F94a7; +$primary1Color: rgba(15, 148, 167, 1); +$primary2Color: rgba(72, 85, 102, 1); +$primary3Color: rgba(184, 222, 228, 1); +$accent1Color: rgba(145, 195, 131, 1); +$alternateTextColor: rgba(255, 255, 255, 1); +$borderColor: rgba(218, 221, 224, 1); +$textColor: $primary2Color; + :export { primary1Color: $primary1Color; } -$primary2Color: #485566; :export { primary2Color: $primary2Color; } -$accent1Color: #91C383; +:export { primary3Color: $primary3Color; } :export { accent1Color: $accent1Color; } -$alternateTextColor: #FFFFFF; :export { alternateTextColor: $alternateTextColor; } -$borderColor: #DADDE0; :export { borderColor: $borderColor; } -$textColor: #485566; :export { textColor: $textColor; } diff --git a/src/styles/muiTheme.js b/src/styles/muiTheme.js index 1dde9c0..c06d5da 100644 --- a/src/styles/muiTheme.js +++ b/src/styles/muiTheme.js @@ -4,18 +4,33 @@ import colors from './base/_colors.scss'; export const palette = { primary1Color: colors.primary1Color, primary2Color: colors.primary2Color, + primary3Color: colors.primary3Color, accent1Color: colors.accent1Color, + // accent2Color: grey100, + // accent3Color: grey500, + textColor: colors.textColor, + // secondaryTextColor: fade(darkBlack, 0.54), alternateTextColor: colors.alternateTextColor, + // canvasColor: white, borderColor: colors.borderColor, - textColor: colors.textColor, + // disabledColor: fade(darkBlack, 0.3), + // pickerHeaderColor: cyan500, + // clockCircleColor: fade(darkBlack, 0.07), + // shadowColor: fullBlack, }; const muiTheme = getMuiTheme({ - fontFamily: 'Libre Franklin, sans-serif', palette, + fontFamily: 'Libre Franklin, sans-serif', raisedButton: { border: '1px solid #FFF', }, + toolbar: { + titleFontSize: '1rem', + separatorColor: palette.primary3Color, + backgroundColor: palette.primary1Color, + iconColor: palette.alternateTextColor, + }, }); export default muiTheme;