From abc4a9ebecb59298ceb781962fb4c5a40b559840 Mon Sep 17 00:00:00 2001 From: "Jana E. Beck" Date: Fri, 22 Jan 2016 12:46:58 -0800 Subject: [PATCH] working rendering of UploadList (w/o events connected up); refinement of behavior on switching target user --- .babelrc | 3 + lib/components/Upload.js | 6 +- lib/components/UploadList.js | 139 +++----- lib/containers/App.js | 82 +++-- lib/redux/actions/async.js | 116 +++++-- lib/redux/actions/sync.js | 8 + lib/redux/constants/actionSources.js | 1 + lib/redux/constants/actionTypes.js | 1 + lib/redux/reducers/reducers.js | 10 + styles/components/UploadList.less | 2 +- test/browser/redux/actions/async.test.js | 314 ++++++++++++++++++- test/browser/redux/actions/sync.test.js | 21 ++ test/browser/redux/reducers/reducers.test.js | 26 ++ 13 files changed, 557 insertions(+), 172 deletions(-) create mode 100644 .babelrc diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..7347f66f20 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + stage: 0 +} \ No newline at end of file diff --git a/lib/components/Upload.js b/lib/components/Upload.js index 888aa7ee4a..64e8a3a5f7 100644 --- a/lib/components/Upload.js +++ b/lib/components/Upload.js @@ -20,8 +20,8 @@ var React = require('react'); var sundial = require('sundial'); var getIn = require('../core/getIn'); var deviceInfo = require('../core/deviceInfo'); -var ProgressBar = require('./ProgressBar.jsx'); -var LoadingBar = require('./LoadingBar.jsx'); +var ProgressBar = require('./ProgressBar'); +var LoadingBar = require('./LoadingBar'); var Upload = React.createClass({ propTypes: { @@ -318,7 +318,7 @@ var Upload = React.createClass({ }, isCarelinkUpload: function() { - return this.props.upload.carelink; + return this.props.upload.key === 'carelink'; }, isFetchingCarelinkData: function() { diff --git a/lib/components/UploadList.js b/lib/components/UploadList.js index a0fcffca33..1e5163feec 100644 --- a/lib/components/UploadList.js +++ b/lib/components/UploadList.js @@ -1,6 +1,7 @@ + /* * == BSD2 LICENSE == -* Copyright (c) 2014, Tidepool Project +* Copyright (c) 2016, Tidepool Project * * This program is free software; you can redistribute it and/or modify it under * the terms of the associated License, which is identical to the BSD 2-Clause @@ -15,117 +16,57 @@ * == BSD2 LICENSE == */ -var _ = require('lodash'); -var React = require('react'); -var cx = require('classnames'); -var Upload = require('./Upload.jsx'); +import _ from 'lodash'; +import React, { Component, PropTypes } from 'react'; +import cx from 'classnames'; -var UploadList = React.createClass({ - propTypes: { - uploads: React.PropTypes.array.isRequired, - targetedUploads: React.PropTypes.array.isRequired, - onUpload: React.PropTypes.func.isRequired, - onReset: React.PropTypes.func.isRequired, - readFile: React.PropTypes.func.isRequired, - groupsDropdown: React.PropTypes.bool.isRequired, - text: React.PropTypes.object - }, - getInitialState: function() { - return { - showErrorDetails: [] - }; - }, - getDefaultProps: function(){ - return { - text: { - SHOW_ERROR : 'Error details', - HIDE_ERROR : 'Hide details', - UPLOAD_FAILED : 'Upload Failed: ' - } - }; - }, - makeHandleShowDetailsFn: function(upload){ - var self = this; +import Upload from './Upload'; - return function(e) { - if(e){ - e.preventDefault(); - } - // add or remove this upload's key to the list of uploads to show errors for - var showErrorsList = self.state.showErrorDetails; - if (_.includes(showErrorsList, upload.key)) { - showErrorsList = _.reject(showErrorsList, function(i) { return i === upload.key; }); - } - else { - showErrorsList.push(upload.key); - } - self.setState({showErrorDetails: showErrorsList}); - }; - }, - renderErrorForUpload: function(upload) { - if (_.isEmpty(upload) || _.isEmpty(upload.error)) { - return; - } - var showDetailsThisUpload = _.includes(this.state.showErrorDetails, upload.key); - var errorDetails = showDetailsThisUpload ? (
{upload.error.debug}
) : null; - var showErrorsText = showDetailsThisUpload ? this.props.text.HIDE_ERROR : this.props.text.SHOW_ERROR; - var errorMessage = upload.error.driverLink ?
- {this.props.text.UPLOAD_FAILED} - {upload.error.friendlyMessage} - {upload.error.driverName} -
: - {this.props.text.UPLOAD_FAILED + upload.error.friendlyMessage}; +export default class UploadList extends Component { + static propTypes = { + devices: PropTypes.object.isRequired, + potentialUploads: PropTypes.array.isRequired, + // targetId can be null when logged in user is not a data storage account + // for example a clinic worker + targetId: PropTypes.string, + userDropdownShowing: PropTypes.bool.isRequired + }; - var clickHandler = this.makeHandleShowDetailsFn(upload); + static defaultProps = { + SHOW_ERROR : 'Error details', + HIDE_ERROR : 'Hide details', + UPLOAD_FAILED : 'Upload Failed: ' + }; - return ( -
- {errorMessage} -
{showErrorsText}
- {errorDetails} -
- ); - }, - render: function() { - var self = this; - var uploadListClasses = cx({ + constructor(props) { + super(props); + } + + render() { + const uploadListClasses = cx({ UploadList: true, - 'UploadList--onlyme': !this.props.groupsDropdown, - 'UploadList--groups': this.props.groupsDropdown + 'UploadList--onlyme': !this.props.userDropdownShowing, + 'UploadList--selectuser': this.props.userDropdownShowing }); - var nodes = _.map(this.props.targetedUploads, function(target){ - var keyToMatch; - var index = _.findIndex(self.props.uploads, function(upload) { - if(upload.key === target.key){ - keyToMatch = target.key; - return true; - } - return false; - }); - var matchingUpload = _.find(self.props.targetedUploads, function(upload) { - return upload.key === keyToMatch; - }); + const { devices } = this.props; + + const items = _.map(this.props.potentialUploads, (deviceKey) => { return ( -
+
- {self.renderErrorForUpload(matchingUpload)} + upload={devices[deviceKey]} + onReset={_.noop} + onUpload={_.noop} + readFile={_.noop} />
); }); return ( -
-
- {nodes} -
+
+ {items}
- ); + ); } -}); - -module.exports = UploadList; +} \ No newline at end of file diff --git a/lib/containers/App.js b/lib/containers/App.js index e6adc05a0a..add5bec874 100644 --- a/lib/containers/App.js +++ b/lib/containers/App.js @@ -41,20 +41,21 @@ import Loading from '../components/Loading'; import Login from '../components/Login'; import LoggedInAs from '../components/LoggedInAs'; import TimezoneDropdown from '../components/TimezoneDropdown'; +import UploadList from '../components/UploadList'; import UserDropdown from '../components/UserDropdown'; export default class App extends Component { + static propTypes = { + api: PropTypes.func.isRequired + }; + constructor(props) { super(props); this.log = bows('App'); this.handleClickChooseDevices = this.handleClickChooseDevices.bind(this); this.handleDismissDropdown = this.handleDismissDropdown.bind(this); - } - - componentWillMount() { - const { api } = this.props; this.props.async.doAppInit(config, { - api, + api: props.api, carelink, device, localStore, @@ -114,7 +115,7 @@ export default class App extends Component { } renderPage() { - const { devices, os, page, showingUserSelectionDropdown, url, users } = this.props; + const { page, showingUserSelectionDropdown, users } = this.props; let userDropdown = showingUserSelectionDropdown ? this.renderUserDropdown() : null; @@ -125,22 +126,30 @@ export default class App extends Component { return ( ); } else if (page === pages.MAIN) { - return null; + return ( +
+ {userDropdown} + +
+ ); } else if (page === pages.SETTINGS) { let timezoneDropdown = this.renderTimezoneDropdown(); - const targetUser = users[users.uploadTargetUser] || {}; return (
{userDropdown} {timezoneDropdown} @@ -199,6 +208,34 @@ App.propTypes = { // wrap the component to inject dispatch and state into it export default connect( (state) => { + function hasSomeoneLoggedIn(state) { + return !_.includes([pages.LOADING, pages.LOGIN], state.page); + } + function getPotentialUploadsForUploadTargetUser(state) { + return Object.keys( + _.get(state, ['uploads', state.users.uploadTargetUser], {}) + ); + } + function getSelectedTargetDevices(state) { + return _.get( + state.users[state.users.uploadTargetUser], + ['targets', 'devices'], + // fall back to the targets stored under 'noUserSelected', if any + _.get(state.users['noUserSelected'], ['targets', 'devices'], []) + ); + } + function getSelectedTimezone(state) { + return _.get( + state.users[state.users.uploadTargetUser], + ['targets', 'timezone'], + // fall back to the timezone stored under 'noUserSelected', if any + _.get(state.users['noUserSelected'], ['targets', 'timezone'], null) + ); + } + function shouldShowUserSelectionDropdown(state) { + return !_.isEmpty(state.users.targetsForUpload) && + state.users.targetsForUpload.length > 1; + } return { // plain state devices: state.devices, @@ -206,24 +243,15 @@ export default connect( os: state.os, page: state.page, version: state.version, + uploads: state.uploads, url: state.url, users: state.users, // derived state - isLoggedIn: !_.includes([pages.LOADING, pages.LOGIN], state.page), - selectedTargetDevices: _.get( - state.users[state.users.uploadTargetUser], - ['targets', 'devices'], - // fall back to the targets stored under 'noUserSelected', if any - _.get(state.users['noUserSelected'], ['targets', 'devices'], []) - ), - selectedTimezone: _.get( - state.users[state.users.uploadTargetUser], - ['targets', 'timezone'], - // fall back to the timezone stored under 'noUserSelected', if any - _.get(state.users['noUserSelected'], ['targets', 'timezone'], null) - ), - showingUserSelectionDropdown: !_.isEmpty(state.users.targetsForUpload) && - state.users.targetsForUpload.length > 1 + isLoggedIn: hasSomeoneLoggedIn(state), + potentialUploads: getPotentialUploadsForUploadTargetUser(state), + selectedTargetDevices: getSelectedTargetDevices(state), + selectedTimezone: getSelectedTimezone(state), + showingUserSelectionDropdown: shouldShowUserSelectionDropdown(state) }; }, (dispatch) => { diff --git a/lib/redux/actions/async.js b/lib/redux/actions/async.js index 430fd9daa3..c9fcb80bbb 100644 --- a/lib/redux/actions/async.js +++ b/lib/redux/actions/async.js @@ -30,7 +30,7 @@ let services = {}; let versionInfo = {}; /* - * ACTION CREATORS + * ASYNCHRONOUS ACTION CREATORS */ export function doAppInit(config, servicesToInit) { @@ -163,38 +163,20 @@ export function doLogout() { }; } -export function retrieveTargetsFromStorage() { - return function(dispatch, getState) { - const { devices, users } = getState(); - const { localStore } = services; - dispatch(syncActions.retrieveUsersTargetsFromStorage()); - const targets = localStore.getItem('devices'); - if (targets === null) { - return dispatch(syncActions.setPage(pages.SETTINGS)); - } - dispatch(syncActions.setUsersTargets(targets)); +/* + * COMPLEX ACTION CREATORS + */ - if (users.uploadTargetUser === null) { - return dispatch(syncActions.setPage(pages.MAIN)); - } - else { - if (targets[users.uploadTargetUser] != null) { - const userTargets = targets[users.uploadTargetUser]; - const targetDeviceKeys = _.pluck(userTargets, 'key'); - const supportedDeviceKeys = Object.keys(devices); - const atLeastOneDeviceSupportedOnSystem = _.some(targetDeviceKeys, function(key) { - return _.includes(supportedDeviceKeys, key); - }); - const uniqTimezones = _.uniq(_.pluck(targets, 'timezone')); - if (uniqTimezones.length === 1 && atLeastOneDeviceSupportedOnSystem) { - dispatch(syncActions.setPage(pages.MAIN)); - } - else { - dispatch(syncActions.setPage(pages.SETTINGS)); - } - } - } - }; +function getUploadsByUser(targetsByUser) { + let uploadsByUser = _.mapValues(targetsByUser, function(targets) { + let uploads = {}; + _.each(targets, function(target) { + uploads[target.key] = {}; + }); + return uploads; + }); + + return uploadsByUser; } export function putTargetsInStorage() { @@ -225,8 +207,76 @@ export function putTargetsInStorage() { Object.assign({}, devicesInStorage, targetsByUser) ); - if (!_.isEmpty(users[users.uploadTargetUser].targets.timezone)) { + if (!_.isEmpty(users[users.uploadTargetUser].targets.timezone) && + !_.isEmpty(users[users.uploadTargetUser].targets.devices)) { dispatch(syncActions.setPage(pages.MAIN)); } + + const uploadsByUser = getUploadsByUser(targetsByUser); + + dispatch(syncActions.setUploads(uploadsByUser)); + }; +} + +export function retrieveTargetsFromStorage() { + return function(dispatch, getState) { + const { devices, users } = getState(); + const { localStore } = services; + dispatch(syncActions.retrieveUsersTargetsFromStorage()); + const targets = localStore.getItem('devices'); + if (targets === null) { + return dispatch(syncActions.setPage(pages.SETTINGS)); + } + else { + const uploadsByUser = getUploadsByUser(targets); + + dispatch(syncActions.setUploads(uploadsByUser)); + } + dispatch(syncActions.setUsersTargets(targets)); + + if (targets[users.uploadTargetUser] != null) { + const userTargets = targets[users.uploadTargetUser]; + const targetDeviceKeys = _.pluck(userTargets, 'key'); + const supportedDeviceKeys = Object.keys(devices); + const atLeastOneDeviceSupportedOnSystem = _.some(targetDeviceKeys, function(key) { + return _.includes(supportedDeviceKeys, key); + }); + let timezones = []; + _.each(userTargets, function(target) { + if (target.timezone) { + timezones.push(target.timezone); + } + }); + let uniqTimezones = []; + if (!_.isEmpty(timezones)) { + uniqTimezones = _.uniq(timezones); + } + if (uniqTimezones.length === 1 && atLeastOneDeviceSupportedOnSystem) { + return dispatch(syncActions.setPage(pages.MAIN)); + } + else { + return dispatch(syncActions.setPage(pages.SETTINGS)); + } + } + dispatch(syncActions.setPage(pages.SETTINGS)); + }; +} + +export function setUploadTargetUserAndMaybeRedirect(targetId) { + return function(dispatch, getState) { + const { devices, users } = getState(); + dispatch(syncActions.setUploadTargetUser(targetId)); + const targetDevices = _.get(users, [targetId, 'targets', 'devices'], []); + const targetTimezone = _.get(users, [targetId, 'targets', 'timezone'], null); + const supportedDeviceKeys = Object.keys(devices); + const atLeastOneDeviceSupportedOnSystem = _.some(targetDevices, function(key) { + return _.includes(supportedDeviceKeys, key); + }); + if (_.isEmpty(targetDevices) || _.isEmpty(targetTimezone)) { + return dispatch(syncActions.setPage(pages.SETTINGS)); + } + if (!atLeastOneDeviceSupportedOnSystem) { + return dispatch(syncActions.setPage(pages.SETTINGS)); + } }; } diff --git a/lib/redux/actions/sync.js b/lib/redux/actions/sync.js index 7a71e1e84f..8d816809f6 100644 --- a/lib/redux/actions/sync.js +++ b/lib/redux/actions/sync.js @@ -86,6 +86,14 @@ export function setTargetTimezone(userId, timezoneName) { }; } +export function setUploads(uploadsByUser) { + return { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }; +} + export function setUploadTargetUser(userId) { return { type: actionTypes.SET_UPLOAD_TARGET_USER, diff --git a/lib/redux/constants/actionSources.js b/lib/redux/constants/actionSources.js index 3fbbe6c3aa..5b0ea7b9e9 100644 --- a/lib/redux/constants/actionSources.js +++ b/lib/redux/constants/actionSources.js @@ -32,6 +32,7 @@ export const SET_OS = UNDER_THE_HOOD; export const SET_PAGE = USER_VISIBLE; export const SET_SIGNUP_URL = USER_VISIBLE; export const SET_TARGET_TIMEZONE = USER; +export const SET_UPLOADS = UNDER_THE_HOOD; export const SET_UPLOAD_TARGET_USER = USER; export const SET_USER_INFO_FROM_TOKEN = USER_VISIBLE; export const SET_USERS_TARGETS = USER_VISIBLE; diff --git a/lib/redux/constants/actionTypes.js b/lib/redux/constants/actionTypes.js index 75fa1e0b25..6798069470 100644 --- a/lib/redux/constants/actionTypes.js +++ b/lib/redux/constants/actionTypes.js @@ -28,6 +28,7 @@ export const SET_OS = 'SET_OS'; export const SET_PAGE = 'SET_PAGE'; export const SET_SIGNUP_URL = 'SET_SIGNUP_URL'; export const SET_TARGET_TIMEZONE = 'SET_TARGET_TIMEZONE'; +export const SET_UPLOADS = 'SET_UPLOADS'; export const SET_UPLOAD_TARGET_USER = 'SET_UPLOAD_TARGET_USER'; export const SET_USER_INFO_FROM_TOKEN = 'SET_USER_INFO_FROM_TOKEN'; export const SET_USERS_TARGETS = 'SET_USERS_TARGETS'; diff --git a/lib/redux/reducers/reducers.js b/lib/redux/reducers/reducers.js index 58867d4e39..573ff8634d 100644 --- a/lib/redux/reducers/reducers.js +++ b/lib/redux/reducers/reducers.js @@ -70,6 +70,16 @@ export function page(state = pages.LOADING, action) { } } +export function uploads(state = {}, action) { + switch (action.type) { + case actionTypes.SET_UPLOADS: + const { uploadsByUser } = action.payload; + return Object.assign({}, state, uploadsByUser); + default: + return state; + } +} + export function url(state = {}, action) { switch (action.type) { case actionTypes.SET_FORGOT_PASSWORD_URL: diff --git a/styles/components/UploadList.less b/styles/components/UploadList.less index 12199aab8d..71c8ad109b 100644 --- a/styles/components/UploadList.less +++ b/styles/components/UploadList.less @@ -61,7 +61,7 @@ max-height: @base-height + @groups-dropdown-height; } -.UploadList--groups { +.UploadList--selectuser { max-height: @base-height; } diff --git a/test/browser/redux/actions/async.test.js b/test/browser/redux/actions/async.test.js index 485a26616e..ce31696c0b 100644 --- a/test/browser/redux/actions/async.test.js +++ b/test/browser/redux/actions/async.test.js @@ -509,7 +509,7 @@ describe('Asynchronous Actions', () => { }); describe('targets retrieved, but no user targeted for upload by default', () => { - it('should dispatch SET_USERS_TARGETS, then SET_PAGE (redirect to main page for user selection)', (done) => { + it('should dispatch RETRIEVING_USERS_TARGETS, SET_USERS_TARGETS, SET_UPLOADS, then SET_PAGE (redirect to settings page for user selection)', (done) => { const targets = { abc123: [{key: 'carelink', timezone: 'US/Eastern'}], def456: [ @@ -517,11 +517,208 @@ describe('Asynchronous Actions', () => { {key: 'omnipod', timezone: 'US/Mountain'} ] }; + const uploadsByUser = { + abc123: { + carelink: {} + }, + def456: { + dexcom: {}, + omnipod: {} + } + }; + const expectedActions = [ + { + type: actionTypes.RETRIEVING_USERS_TARGETS, + meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]} + }, + { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }, + { + type: actionTypes.SET_USERS_TARGETS, + payload: { targets }, + meta: {source: actionSources[actionTypes.SET_USERS_TARGETS]} + }, + { + type: actionTypes.SET_PAGE, + payload: {page: pages.SETTINGS}, + meta: {source: actionSources[actionTypes.SET_PAGE]} + } + ]; + asyncActions.__Rewire__('services', { + localStore: { + getItem: () => targets + } + }); + const store = mockStore({ + users: { + loggedInUser: 'ghi789', + ghi789: {}, + abc123: {}, + def456: {}, + targetsForUpload: ['abc123', 'def456'], + uploadTargetUser: null + } + }, expectedActions, done); + store.dispatch(asyncActions.retrieveTargetsFromStorage()); + }); + }); + + describe('targets retrieved, user targeted for upload is missing timezone', () => { + it('should dispatch RETRIEVING_USERS_TARGETS, SET_USERS_TARGETS, SET_UPLOADS, then SET_PAGE (redirect to settings page for timezone selection)', (done) => { + const targets = { + abc123: [{key: 'carelink'}], + def456: [ + {key: 'dexcom', timezone: 'US/Mountain'}, + {key: 'omnipod', timezone: 'US/Mountain'} + ] + }; + const uploadsByUser = { + abc123: { + carelink: {} + }, + def456: { + dexcom: {}, + omnipod: {} + } + }; + const expectedActions = [ + { + type: actionTypes.RETRIEVING_USERS_TARGETS, + meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]} + }, + { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }, + { + type: actionTypes.SET_USERS_TARGETS, + payload: { targets }, + meta: {source: actionSources[actionTypes.SET_USERS_TARGETS]} + }, + { + type: actionTypes.SET_PAGE, + payload: {page: pages.SETTINGS}, + meta: {source: actionSources[actionTypes.SET_PAGE]} + } + ]; + asyncActions.__Rewire__('services', { + localStore: { + getItem: () => targets + } + }); + const store = mockStore({ + devices: { + carelink: {}, + dexcom: {}, + omnipod: {} + }, + users: { + loggedInUser: 'ghi789', + ghi789: {}, + abc123: {}, + def456: {}, + targetsForUpload: ['abc123', 'def456'], + uploadTargetUser: 'abc123' + } + }, expectedActions, done); + store.dispatch(asyncActions.retrieveTargetsFromStorage()); + }); + }); + + describe('targets retrieved, user targeted for upload has no supported devices', () => { + it('should dispatch RETRIEVING_USERS_TARGETS, SET_USERS_TARGETS, SET_UPLOADS, then SET_PAGE (redirect to settings page for device selection)', (done) => { + const targets = { + abc123: [{key: 'carelink', timezone: 'US/Eastern'}], + def456: [ + {key: 'dexcom', timezone: 'US/Mountain'}, + {key: 'omnipod', timezone: 'US/Mountain'} + ] + }; + const uploadsByUser = { + abc123: { + carelink: {} + }, + def456: { + dexcom: {}, + omnipod: {} + } + }; + const expectedActions = [ + { + type: actionTypes.RETRIEVING_USERS_TARGETS, + meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]} + }, + { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }, + { + type: actionTypes.SET_USERS_TARGETS, + payload: { targets }, + meta: {source: actionSources[actionTypes.SET_USERS_TARGETS]} + }, + { + type: actionTypes.SET_PAGE, + payload: {page: pages.SETTINGS}, + meta: {source: actionSources[actionTypes.SET_PAGE]} + } + ]; + asyncActions.__Rewire__('services', { + localStore: { + getItem: () => targets + } + }); + const store = mockStore({ + devices: { + dexcom: {}, + omnipod: {} + }, + users: { + loggedInUser: 'ghi789', + ghi789: {}, + abc123: {}, + def456: {}, + targetsForUpload: ['abc123', 'def456'], + uploadTargetUser: 'abc123' + } + }, expectedActions, done); + store.dispatch(asyncActions.retrieveTargetsFromStorage()); + }); + }); + + describe('targets retrieved, user targeted for upload is all set to upload', () => { + it('should dispatch RETRIEVING_USERS_TARGETS, SET_USERS_TARGETS, SET_UPLOADS, then SET_PAGE (redirect to main page)', (done) => { + const targets = { + abc123: [{key: 'carelink', timezone: 'US/Eastern'}], + def456: [ + {key: 'dexcom', timezone: 'US/Mountain'}, + {key: 'omnipod', timezone: 'US/Mountain'} + ] + }; + const uploadsByUser = { + abc123: { + carelink: {} + }, + def456: { + dexcom: {}, + omnipod: {} + } + }; const expectedActions = [ { type: actionTypes.RETRIEVING_USERS_TARGETS, meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]} }, + { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }, { type: actionTypes.SET_USERS_TARGETS, payload: { targets }, @@ -538,16 +735,115 @@ describe('Asynchronous Actions', () => { getItem: () => targets } }); - const store = mockStore({users: { - loggedInUser: 'ghi789', - ghi789: {}, - abc123: {}, - def456: {}, - targetsForUpload: ['abc123', 'def456'], - uploadTargetUser: null - }}, expectedActions, done); + const store = mockStore({ + devices: { + carelink: {}, + dexcom: {}, + omnipod: {} + }, + users: { + loggedInUser: 'ghi789', + ghi789: {}, + abc123: {}, + def456: {}, + targetsForUpload: ['abc123', 'def456'], + uploadTargetUser: 'abc123' + } + }, expectedActions, done); store.dispatch(asyncActions.retrieveTargetsFromStorage()); }); }); }); + + describe('setUploadTargetUserAndMaybeRedirect', () => { + describe('new target user has selected devices and timezone', () => { + it('should dispatch just SET_UPLOAD_TARGET_USER', (done) => { + const userId = 'abc123'; + const expectedActions = [ + { + type: actionTypes.SET_UPLOAD_TARGET_USER, + payload: { userId }, + meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]} + } + ]; + const store = mockStore({ + users: { + abc123: { + targets: { + devices: ['a_pump'], + timezone: 'Europe/London' + } + } + } + }, expectedActions, done); + store.dispatch(asyncActions.setUploadTargetUserAndMaybeRedirect(userId)); + }); + }); + + describe('new target user has not selected devices', () => { + it('should dispatch just SET_UPLOAD_TARGET_USER, SET_PAGE (redirect to settings)', (done) => { + const userId = 'abc123'; + const expectedActions = [ + { + type: actionTypes.SET_UPLOAD_TARGET_USER, + payload: { userId }, + meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]} + }, + { + type: actionTypes.SET_PAGE, + payload: {page: pages.SETTINGS}, + meta: {source: actionSources[actionTypes.SET_PAGE]} + } + ]; + const store = mockStore({ + devices: { + carelink: {}, + dexcom: {}, + omnipod: {} + }, + users: { + abc123: { + targets: { + timezone: 'Europe/London' + } + } + } + }, expectedActions, done); + store.dispatch(asyncActions.setUploadTargetUserAndMaybeRedirect(userId)); + }); + }); + + describe('new target user has not selected timezone', () => { + it('should dispatch just SET_UPLOAD_TARGET_USER, SET_PAGE (redirect to settings)', (done) => { + const userId = 'abc123'; + const expectedActions = [ + { + type: actionTypes.SET_UPLOAD_TARGET_USER, + payload: { userId }, + meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]} + }, + { + type: actionTypes.SET_PAGE, + payload: {page: pages.SETTINGS}, + meta: {source: actionSources[actionTypes.SET_PAGE]} + } + ]; + const store = mockStore({ + devices: { + carelink: {}, + dexcom: {}, + omnipod: {} + }, + users: { + abc123: { + targets: { + devices: ['carelink'] + } + } + } + }, expectedActions, done); + store.dispatch(asyncActions.setUploadTargetUserAndMaybeRedirect(userId)); + }); + }); + }); }); \ No newline at end of file diff --git a/test/browser/redux/actions/sync.test.js b/test/browser/redux/actions/sync.test.js index 63953f3dce..bf23891394 100644 --- a/test/browser/redux/actions/sync.test.js +++ b/test/browser/redux/actions/sync.test.js @@ -180,6 +180,27 @@ describe('Synchronous Actions', () => { }); }); + describe('setUploads', () => { + const uploadsByUser = { + a1b2c3: {a_pump: {}, a_cgm: {}}, + d4e5f6: {another_pump: {}} + }; + it('should be an FSA', () => { + let action = syncActions.setUploads(uploadsByUser); + + expect(isFSA(action)).to.be.true; + }); + + it('should create an action to set up the potential uploads for each user reflecting target devices selected', () => { + const expectedAction = { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser }, + meta: {source: actionSources[actionTypes.SET_UPLOADS]} + }; + expect(syncActions.setUploads(uploadsByUser)).to.deep.equal(expectedAction); + }); + }); + describe('setUploadTargetUser', () => { const ID = 'a1b2c3'; it('should be an FSA', () => { diff --git a/test/browser/redux/reducers/reducers.test.js b/test/browser/redux/reducers/reducers.test.js index 31540168ae..aee2ae2c03 100644 --- a/test/browser/redux/reducers/reducers.test.js +++ b/test/browser/redux/reducers/reducers.test.js @@ -134,6 +134,32 @@ describe('reducers', () => { }); }); + describe('uploads', () => { + it('should return the initial state', () => { + expect(reducers.uploads(undefined, {})).to.deep.equal({}); + }); + + it('should handle SET_UPLOADS', () => { + const uploadsByUser = { + a1b2c3: {a_pump: {}, a_cgm: {}}, + d4e5f6: {another_pump: {}} + }; + const actionPayload = { uploadsByUser }; + expect(reducers.uploads(undefined, { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser } + })).to.deep.equal(uploadsByUser); + let initialState = { + a1b2c3: {a_cgm: {}} + }; + let finalState = reducers.uploads(initialState, { + type: actionTypes.SET_UPLOADS, + payload: { uploadsByUser } + }); + expect(initialState === finalState).to.be.false; + }); + }); + describe('url', () => { it('should return the initial state', () => { expect(reducers.url(undefined, {})).to.deep.equal({});