+
+
{text}
+
{this.groupSelector()}
@@ -89,4 +88,4 @@ var UploadSettings = React.createClass({
}
});
-module.exports = UploadSettings;
+module.exports = UserDropdown;
diff --git a/lib/components/VersionCheckError.js b/lib/components/VersionCheckError.js
new file mode 100644
index 0000000000..321a08e929
--- /dev/null
+++ b/lib/components/VersionCheckError.js
@@ -0,0 +1,69 @@
+/*
+* == BSD2 LICENSE ==
+* 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
+* License as published by the Open Source Initiative at opensource.org.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the License for more details.
+*
+* You should have received a copy of the License along with this program; if
+* not, you can obtain one from Tidepool Project at tidepool.org.
+* == BSD2 LICENSE ==
+*/
+
+import cx from 'classnames';
+import React, { Component, PropTypes } from 'react';
+
+import * as errorUtils from '../redux/utils/errors';
+
+export default class VersionCheckError extends Component {
+ static propTypes = {
+ errorMessage: PropTypes.string.isRequired,
+ errorText: PropTypes.object.isRequired
+ };
+
+ static defaultProps = {
+ errorText: {
+ CONNECT: 'Please check your connection, quit & relaunch to try again.',
+ ERROR_DETAILS: 'Details for Tidepool\'s developers:',
+ OFFLINE: 'You\'re not connected to the Internet.',
+ SERVERS_DOWN: 'Tidepool\'s servers are down.',
+ TRY_AGAIN: 'In a few minutes, please quit & relaunch to try again.'
+ }
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { errorMessage, errorText } = this.props;
+ const offline = errorMessage === errorUtils.errorText.E_OFFLINE;
+ const errorDetails = offline ? null : (
+
+
{errorText.ERROR_DETAILS}
+
{errorMessage}
+
+ );
+ const firstLine = offline ? errorText.OFFLINE : errorText.SERVERS_DOWN;
+ const secondLine = offline ? errorText.CONNECT : errorText.TRY_AGAIN;
+ const versionCheckClass = cx({
+ VersionCheck: true,
+ 'VersionCheck--failed': !offline,
+ 'VersionCheck--offline': offline
+ });
+ return (
+
+
+
{firstLine}
+
{secondLine}
+
+ {errorDetails}
+
+ );
+ }
+};
diff --git a/lib/components/ViewDataLink.jsx b/lib/components/ViewDataLink.js
similarity index 77%
rename from lib/components/ViewDataLink.jsx
rename to lib/components/ViewDataLink.js
index 6283b109f2..fa05eaef27 100644
--- a/lib/components/ViewDataLink.jsx
+++ b/lib/components/ViewDataLink.js
@@ -15,6 +15,7 @@
* == BSD2 LICENSE ==
*/
+var _ = require('lodash');
var React = require('react');
var ViewDataLink = React.createClass({
@@ -24,7 +25,15 @@ var ViewDataLink = React.createClass({
},
render: function() {
- return
Go to Blip;
+ return (
+
+ Go to Blip
+
+ );
}
});
diff --git a/lib/containers/App.js b/lib/containers/App.js
new file mode 100644
index 0000000000..315d7d1a4e
--- /dev/null
+++ b/lib/containers/App.js
@@ -0,0 +1,341 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2014-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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import bows from '../bows.js';
+
+import config from '../config.js';
+
+import carelink from '../core/carelink.js';
+import device from '../core/device.js';
+import localStore from '../core/localStore.js';
+
+import actions from '../redux/actions/';
+const asyncActions = actions.async;
+const syncActions = actions.sync;
+
+import * as actionTypes from '../redux/constants/actionTypes';
+import * as actionSources from '../redux/constants/actionSources';
+import { pages, urls } from '../redux/constants/otherConstants';
+
+import DeviceSelection from '../components/DeviceSelection';
+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 UpdatePlease from '../components/UpdatePlease';
+import UserDropdown from '../components/UserDropdown';
+import VersionCheckError from '../components/VersionCheckError';
+import ViewDataLink from '../components/ViewDataLink';
+
+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);
+ this.props.async.doAppInit(Object.assign({}, config, {os: props.os}), {
+ api: props.api,
+ carelink,
+ device,
+ localStore,
+ log: this.log
+ });
+ }
+
+ render() {
+ const { isLoggedIn, page } = this.props;
+ return (
+
+
{this.renderHeader()}
+
{this.renderPage()}
+
{this.renderFooter()}
+ {/* VersionCheck as overlay */}
+ {this.renderVersionCheck()}
+
+ );
+ }
+
+ handleClickChooseDevices() {
+ const { setPage, toggleDropdown } = this.props.sync;
+ // ensure dropdown closes after click
+ setPage(pages.SETTINGS, true);
+ toggleDropdown(true, actionSources.UNDER_THE_HOOD);
+ }
+
+ handleDismissDropdown() {
+ const { dropdown } = this.props;
+ // only toggle the dropdown by clicking elsewhere if it's open
+ if (dropdown === true) {
+ this.props.sync.toggleDropdown(dropdown);
+ }
+ }
+
+ renderHeader() {
+ const { allUsers, dropdown, isLoggedIn, page } = this.props;
+ if (page === pages.LOADING) {
+ return null;
+ }
+
+ if (!isLoggedIn) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ renderPage() {
+ const { page, unsupported, uploadTargetUser } = this.props;
+
+ let userDropdown = this.props.showingUserSelectionDropdown ?
+ this.renderUserDropdown() : null;
+
+ if (page === pages.LOADING) {
+ return (
);
+ } else if (page === pages.LOGIN) {
+ return (
+
+ );
+ } else if (page === pages.MAIN) {
+ const viewDataLink = _.get(this.props, ['blipUrls', 'viewDataLink'], '');
+ return (
+
+ {userDropdown}
+
+
+
+ );
+ } else if (page === pages.SETTINGS) {
+ let timezoneDropdown = this.renderTimezoneDropdown();
+ return (
+
+ {userDropdown}
+ {timezoneDropdown}
+
+
+ );
+ } else {
+ throw new Error('Unrecognized page!');
+ }
+ }
+
+ renderFooter() {
+ const { version } = this.props;
+ return (
+
+
+
{`v${version} beta`}
+
+ );
+ }
+
+ renderTimezoneDropdown() {
+ const { uploadTargetUser } = this.props;
+ return (
+
+ );
+ }
+
+ renderUserDropdown() {
+ const { allUsers, page, targetUsersForUpload, uploadTargetUser } = this.props;
+ return (
+
+ );
+ }
+
+ renderVersionCheck() {
+ const { readyToRenderVersionCheckOverlay, unsupported } = this.props;
+ if (readyToRenderVersionCheckOverlay === false || unsupported === false) {
+ return null;
+ }
+ if (unsupported instanceof Error) {
+ return (
+
+ );
+ }
+ if (unsupported === true) {
+ return (
+
+ );
+ }
+ }
+}
+
+App.propTypes = {
+ page: React.PropTypes.string.isRequired
+};
+
+// wrap the component to inject dispatch and state into it
+export default connect(
+ (state) => {
+ function getActiveUploads(state) {
+ const { devices, uploadsByUser, uploadTargetUser } = state;
+ if (uploadTargetUser === null) {
+ return [];
+ }
+ let activeUploads = [];
+ const targetUsersUploads = _.get(uploadsByUser, uploadTargetUser, []);
+ _.map(Object.keys(targetUsersUploads), (deviceKey) => {
+ const upload = uploadsByUser[uploadTargetUser][deviceKey];
+ const device = _.pick(devices[deviceKey], ['instructions', 'key', 'name', 'source']);
+ const progress = upload.uploading ? {progress: state.uploadProgress} :
+ (upload.successful ? {progress: {percentage: 100}} : {});
+ activeUploads.push(_.assign({}, device, upload, progress));
+ });
+ return activeUploads;
+ }
+ function hasSomeoneLoggedIn(state) {
+ return !_.includes([pages.LOADING, pages.LOGIN], state.page);
+ }
+ function isUploadInProgress(state) {
+ let blockModePrepInProgress = false;
+ if (state.uploadTargetDevice !== null) {
+ const currentDevice = state.devices[state.uploadTargetDevice];
+ if (currentDevice.source.type === 'block') {
+ let blockModeInProgress = _.get(state.uploadsByUser, [state.uploadTargetUser, currentDevice.key], {});
+ if (blockModeInProgress.choosingFile || blockModeInProgress.readingFile ||
+ _.get(blockModeInProgress, ['file', 'data'], null) !== null) {
+ blockModePrepInProgress = true;
+ }
+ }
+ }
+ if (state.working.uploading || blockModePrepInProgress) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ function getPotentialUploadsForUploadTargetUser(state) {
+ return Object.keys(
+ _.get(state, ['uploadsByUser', state.uploadTargetUser], {})
+ );
+ }
+ function getSelectedTargetDevices(state) {
+ return _.get(
+ state,
+ ['targetDevices', state.uploadTargetUser],
+ // fall back to the targets stored under 'noUserSelected', if any
+ _.get(state, ['targetDevices', 'noUserSelected'], [])
+ );
+ }
+ function getSelectedTimezone(state) {
+ return _.get(
+ state,
+ ['targetTimezones', state.uploadTargetUser],
+ // fall back to the timezone stored under 'noUserSelected', if any
+ _.get(state, ['targetTimezones', 'noUserSelected'], null)
+ );
+ }
+ function shouldShowUserSelectionDropdown(state) {
+ return !_.isEmpty(state.targetUsersForUpload) &&
+ state.targetUsersForUpload.length > 1;
+ }
+ return {
+ // plain state
+ allUsers: state.allUsers,
+ blipUrls: state.blipUrls,
+ devices: state.devices,
+ dropdown: state.dropdown,
+ loggedInUser: state.loggedInUser,
+ loginErrorMessage: state.loginErrorMessage,
+ page: state.page,
+ targetUsersForUpload: state.targetUsersForUpload,
+ unsupported: state.unsupported,
+ uploadIsInProgress: state.working.uploading,
+ uploadsByUser: state.uploadsByUser,
+ uploadTargetUser: state.uploadTargetUser,
+ // derived state
+ activeUploads: getActiveUploads(state),
+ fetchingUserInfo: state.working.fetchingUserInfo,
+ isLoggedIn: hasSomeoneLoggedIn(state),
+ potentialUploads: getPotentialUploadsForUploadTargetUser(state),
+ readyToRenderVersionCheckOverlay: (
+ !state.working.initializingApp && !state.working.checkingVersion
+ ),
+ selectedTargetDevices: getSelectedTargetDevices(state),
+ selectedTimezone: getSelectedTimezone(state),
+ showingUserSelectionDropdown: shouldShowUserSelectionDropdown(state)
+ };
+ },
+ (dispatch) => {
+ return {
+ async: bindActionCreators(asyncActions, dispatch),
+ sync: bindActionCreators(syncActions, dispatch)
+ };
+ }
+)(App);
diff --git a/lib/containers/root/Root.dev.js b/lib/containers/root/Root.dev.js
new file mode 100644
index 0000000000..94e9c589d4
--- /dev/null
+++ b/lib/containers/root/Root.dev.js
@@ -0,0 +1,41 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import React, { Component } from 'react';
+import { Provider } from 'react-redux';
+
+import configureStore from '../../redux/store/configureStore';
+
+import App from '../App';
+import DevTools from '../../components/DevTools';
+
+const { api, store, version } = configureStore();
+
+export default class Root extends Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/lib/containers/root/Root.js b/lib/containers/root/Root.js
new file mode 100644
index 0000000000..78ff298ee7
--- /dev/null
+++ b/lib/containers/root/Root.js
@@ -0,0 +1,24 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/* global __REDUX_DEV_UI__ */
+
+if (__REDUX_DEV_UI__ === true) {
+ module.exports = require('./Root.dev');
+} else {
+ module.exports = require('./Root.prod');
+}
diff --git a/lib/containers/root/Root.prod.js b/lib/containers/root/Root.prod.js
new file mode 100644
index 0000000000..d26188c122
--- /dev/null
+++ b/lib/containers/root/Root.prod.js
@@ -0,0 +1,35 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import React, { Component } from 'react';
+import { Provider } from 'react-redux';
+
+import configureStore from '../../redux/store/configureStore';
+
+import App from '../App';
+
+const { api, store, version } = configureStore();
+
+export default class Root extends Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/lib/core/api.js b/lib/core/api.js
index d29ca61a7b..0a5267af6e 100644
--- a/lib/core/api.js
+++ b/lib/core/api.js
@@ -17,16 +17,19 @@
var _ = require('lodash');
var async = require('async');
+var format = require('util').format;
var md5 = require('blueimp-md5');
// sometimes we load this into node and this routine behaves differently
if (md5.md5) {
md5 = md5.md5;
}
+var semver = require('semver');
var sundial = require('sundial');
var uuid = require('node-uuid');
var isChromeApp = (typeof chrome !== 'undefined');
var bows = require('../bows');
+var errorText = require('../redux/utils/errors').errorText;
var log = isChromeApp ? bows('Api') : console.log;
var builder = require('../objectBuilder')();
var localStore = require('./localStore');
@@ -46,7 +49,8 @@ var api = {
// ----- Api Setup -----
-api.init = function(options, cb) {
+// synchronous!
+api.create = function(options) {
var tidepoolLog = isChromeApp ? bows('Tidepool') : console.log;
tidepool = createTidepoolClient({
host: options.apiUrl,
@@ -58,12 +62,15 @@ api.init = function(options, cb) {
},
localStore: localStore,
metricsSource: 'chrome-uploader',
- metricsVersion: 'chrome-uploader-beta'
+ metricsVersion: options.version
});
api.tidepool = tidepool;
+};
- tidepool.initialize(cb);
+// asynchronous!
+api.init = function(cb) {
+ api.tidepool.initialize(cb);
};
// ----- Config -----
@@ -115,7 +122,19 @@ api.user.profile = function(cb) {
api.user.logout = function(cb) {
api.log('POST /auth/logout');
- tidepool.logout(cb);
+ if (!tidepool.isLoggedIn()) {
+ api.log('Not authenticated, but still destroying session for just in cases...');
+ tidepool.destroySession();
+ return;
+ }
+ tidepool.logout(function(err) {
+ if (err) {
+ api.log('Error while logging out but still destroying session...');
+ tidepool.destroySession();
+ return cb(err);
+ }
+ cb(null);
+ });
};
api.user.getUploadGroups = function(cb) {
@@ -170,6 +189,25 @@ api.user.getUploadGroups = function(cb) {
api.upload = {};
+api.upload.getVersions = function(cb) {
+ api.log('GET /info');
+ tidepool.checkUploadVersions(function(err, resp) {
+ if (err) {
+ if (!navigator.onLine) {
+ return cb(new Error(errorText.E_OFFLINE));
+ }
+ return cb(err);
+ }
+ var uploaderVersion = _.get(resp, ['versions', 'uploaderMinimum'], null);
+ if (uploaderVersion !== null) {
+ return cb(null, resp.versions);
+ }
+ else {
+ return cb(new Error(format('Info response does not contain versions.uploaderMinimum.')));
+ }
+ });
+};
+
api.upload.accounts = function(happyCb, sadCb) {
api.log('GET /access/groups/'+tidepool.getUserId());
tidepool.getViewableUsers(tidepool.getUserId(),function(err, data) {
diff --git a/lib/core/deviceInfo.js b/lib/core/deviceInfo.js
deleted file mode 100644
index 05a8758a03..0000000000
--- a/lib/core/deviceInfo.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * == BSD2 LICENSE ==
- * Copyright (c) 2014, 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
- * License as published by the Open Source Initiative at opensource.org.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the License for more details.
- *
- * You should have received a copy of the License along with this program; if
- * not, you can obtain one from Tidepool Project at tidepool.org.
- * == BSD2 LICENSE ==
- */
-
-function infoBuilder(generalName, detailName) {
- return {
- getName: function(device) {
- return generalName;
- },
- getDetail: function(device) {
- var detail = detailName;
- if (device.serialNumber) {
- detail = detail + '
Serial # ' + device.serialNumber;
- }
- return detail;
- }
- };
-}
-
-module.exports = {
- 'Dexcom': infoBuilder('Dexcom', 'Plug in receiver with micro-USB'),
- 'OneTouchMini': infoBuilder('OneTouch UltraMini', ''),
- 'AbbottPrecisionXtra': infoBuilder('Abbott Precision Xtra',
- 'Plug in meter with cable'),
- 'InsuletOmniPod': infoBuilder('Insulet OmniPod', 'Choose .ibf file from PDM'),
- 'Tandem': infoBuilder('Tandem', 'Plug in pump with micro-USB'),
- 'OneTouchUltra2': infoBuilder('OneTouch Ultra2', ''),
- 'AbbottFreeStyleLite': infoBuilder('Abbott FreeStyle Lite','Plug in meter with cable'),
- 'AbbottFreeStyleFreedomLite': infoBuilder('Abbott FreeStyle Freedom Lite','Plug in meter with cable'),
- 'BayerContourNext': infoBuilder('Bayer Contour Next', 'Plug in meter with micro-USB'),
- 'BayerContourNextUsb': infoBuilder('Bayer Contour Next USB', 'Plug meter into USB port'),
- 'BayerContourUsb': infoBuilder('Bayer Contour USB', 'Plug meter into USB port'),
- 'BayerContourNextLink': infoBuilder('Bayer Contour Next LINK', 'Plug meter into USB port')
-};
diff --git a/lib/core/storage.js b/lib/core/storage.js
index 084841c9a1..7ecf0843d4 100644
--- a/lib/core/storage.js
+++ b/lib/core/storage.js
@@ -13,8 +13,6 @@
// not, you can obtain one from Tidepool Project at tidepool.org.
// == BSD2 LICENSE ==
-'use strict';
-
var _ = require('lodash');
/**
diff --git a/lib/crc.js b/lib/crc.js
index 351b1a5ccb..2f2b81ea9c 100644
--- a/lib/crc.js
+++ b/lib/crc.js
@@ -15,8 +15,6 @@
* == BSD2 LICENSE ==
*/
-/* jshint quotmark: false */
-
module.exports.A_INITIAL_REMAINDER = 0xFFFF;
module.exports.A_FINAL_XOR_VALUE = 0x0000;
module.exports.D_INITIAL_REMAINDER = 0x0000;
@@ -119,7 +117,7 @@ module.exports.testCRC_A = function (s) {
}
console.log(bytes);
var acrc = this.calcCRC_A(bytes, s.length);
- console.log("Asante CRC (decimal) = ", acrc);
+ console.log('Asante CRC (decimal) = ', acrc);
return acrc;
};
@@ -131,7 +129,7 @@ module.exports.testCRC_D = function (s) {
}
console.log(bytes);
var dcrc = this.calcCRC_D(bytes, s.length);
- console.log("Dexcom CRC (decimal) = ", dcrc);
+ console.log('Dexcom CRC (decimal) = ', dcrc);
return dcrc;
};
@@ -139,12 +137,12 @@ module.exports.testCRC_D = function (s) {
module.exports.validateCRC = function () {
// this line of code is straight from Asante's and J&J's documentation as a test case
if (this.testCRC_A('\x02\x06\x06\x03') != 0x41CD) {
- console.log("CRC_A logic is NOT CORRECT!!!");
+ console.log('CRC_A logic is NOT CORRECT!!!');
return false;
}
// this is backsolved from Dexcom code that works to mimic the test case above.
if (this.testCRC_D('\x02\x06\x06\x03') != 50445) {
- console.log("CRC_D logic is NOT CORRECT!!!");
+ console.log('CRC_D logic is NOT CORRECT!!!');
return false;
}
return true;
diff --git a/lib/drivers/docs/README.md b/lib/drivers/docs/README.md
new file mode 100644
index 0000000000..92142f437e
--- /dev/null
+++ b/lib/drivers/docs/README.md
@@ -0,0 +1,11 @@
+Checklists for the implementation of drivers for reading data from diabetes devices currently supported or in development.
+
+ * [Abbott FreeStyle (BGM)](abbottFreeStyleLite.md)
+ * [Abbott Precision Xtra (blood glucose & ketone meter)](abbottPrecisionXtra.md)
+ * [Animas Ping and Vibe Insulin Pumps](animasPingAndVibe.md)
+ * [Bayer Contour Next (BGM)](bayerContourNext.md)
+ * [CareLink (CGM data)](carelinkCGM.md)
+ * [CareLink (insulin pump data)](carelinkPumpData.md)
+ * [Dexcom CGM](dexcom.md)
+ * [Insulet OmniPod Insulin Delivery System](insuletOmniPod.md)
+ * [Tandem Insulin Pumps](tandem.md)
\ No newline at end of file
diff --git a/lib/drivers/docs/animasPingAndVibe.md b/lib/drivers/docs/animasPingAndVibe.md
index 00ddc16438..3256d1417f 100644
--- a/lib/drivers/docs/animasPingAndVibe.md
+++ b/lib/drivers/docs/animasPingAndVibe.md
@@ -68,7 +68,7 @@ Device-specific? (Add any device-specific notes/additions here.)
#### CBG
-(See [the CGM checklist](CGMChecklist.md) instead.)
+(See [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead.)
#### Device Events
@@ -99,7 +99,7 @@ Device-specific? (Add any device-specific notes/additions here.)
- `*[-]` suspensions of insulin delivery are represented as pairs of point-in-time events: a suspension and a resumption
- `[ ]` reason/agent of suspension (`automatic` or `manual`)
- `[ ]` reason/agent of resumption (`automatic` or `manual`)
- - calibrations: see [the CGM checklist](CGMChecklist.md) instead
+ - calibrations: see [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead
- `[ ]` time changes (presence of which is also in the [BtUTC section](#bootstrapping-to-utc) below)
- `[ ]` device display time `from` (before change) and `to` (result of change)
- `[ ]` agent of change (`automatic` or `manual`)
diff --git a/lib/drivers/docs/carelinkPumpData.md b/lib/drivers/docs/carelinkPumpData.md
index 61eb52c02a..00571f8ac9 100644
--- a/lib/drivers/docs/carelinkPumpData.md
+++ b/lib/drivers/docs/carelinkPumpData.md
@@ -70,7 +70,7 @@ Device-specific? (Add any device-specific notes/additions here.)
#### CBG
-(See [the CGM checklist](CGMChecklist.md) instead.)
+(See [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead.)
#### Device Events
@@ -101,7 +101,7 @@ Device-specific? (Add any device-specific notes/additions here.)
- `[x]` suspensions of insulin delivery are represented as pairs of point-in-time events: a suspension and a resumption
- `[x]` reason/agent of suspension (`automatic` or `manual`)
- `[x]` reason/agent of resumption (`automatic` or `manual`)
- - calibrations: see [the CGM checklist](CGMChecklist.md) instead
+ - calibrations: see [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead
- `[x]` time changes (presence of which is also in the [BtUTC section](#bootstrapping-to-utc) below)
- `[x]` device display time `from` (before change) and `to` (result of change)
- `[x]` agent of change (`automatic` or `manual`)
diff --git a/lib/drivers/docs/insuletOmniPod.md b/lib/drivers/docs/insuletOmniPod.md
index 776e23fbc6..637bd52fb1 100644
--- a/lib/drivers/docs/insuletOmniPod.md
+++ b/lib/drivers/docs/insuletOmniPod.md
@@ -68,7 +68,7 @@ Device-specific? (Add any device-specific notes/additions here.)
#### CBG
-(See [the CGM checklist](CGMChecklist.md) instead.)
+(See [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead.)
#### Device Events
@@ -99,7 +99,7 @@ Device-specific? (Add any device-specific notes/additions here.)
- `[x]` suspensions of insulin delivery are represented as pairs of point-in-time events: a suspension and a resumption
- `[ ]` reason/agent of suspension (`automatic` or `manual`)
- `[ ]` reason/agent of resumption (`automatic` or `manual`)
- - calibrations: see [the CGM checklist](CGMChecklist.md) instead
+ - calibrations: see [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead
- `[x]` time changes (presence of which is also in the [BtUTC section](#bootstrapping-to-utc) below)
- `[x]` device display time `from` (before change) and `to` (result of change)
- `[x]` agent of change (`automatic` or `manual`)
diff --git a/lib/drivers/docs/tandem.md b/lib/drivers/docs/tandem.md
index e88d0f86f7..904632042f 100644
--- a/lib/drivers/docs/tandem.md
+++ b/lib/drivers/docs/tandem.md
@@ -72,7 +72,7 @@ Device-specific? (Add any device-specific notes/additions here.)
#### CBG
-(See [the CGM checklist](CGMChecklist.md) instead.)
+(See [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead.)
#### Device Events
@@ -103,7 +103,7 @@ Device-specific? (Add any device-specific notes/additions here.)
- `[x]` suspensions of insulin delivery are represented as pairs of point-in-time events: a suspension and a resumption
- `[x]` reason/agent of suspension (`automatic` or `manual`)
- `[ ]` reason/agent of resumption (`automatic` or `manual`)
- - calibrations: see [the CGM checklist](CGMChecklist.md) instead
+ - calibrations: see [the CGM checklist](../../../docs/checklisttemplates/CGMChecklist.md) instead
- `[x]` time changes (presence of which is also in the [BtUTC section](#bootstrapping-to-utc) below)
- `[x]` device display time `from` (before change) and `to` (result of change)
- `[x]` agent of change (`automatic` or `manual`)
diff --git a/lib/drivers/insuletDriver.js b/lib/drivers/insuletDriver.js
index a5c4b2bd94..c0ca64d133 100644
--- a/lib/drivers/insuletDriver.js
+++ b/lib/drivers/insuletDriver.js
@@ -1653,7 +1653,6 @@ module.exports = function (config) {
}
}
simulator.finalBasal();
- data.post_records = [];
var sessionInfo = {
deviceTags: ['insulin-pump', 'bgm'],
@@ -1667,8 +1666,10 @@ module.exports = function (config) {
version: cfg.version
};
+ data.post_records = simulator.getEvents();
+
cfg.api.upload.toPlatform(
- simulator.getEvents(),
+ data.post_records,
sessionInfo,
progress,
cfg.groupId,
@@ -1679,7 +1680,6 @@ module.exports = function (config) {
return cb(err, data);
} else {
progress(100);
- data.post_records = data.post_records.concat(postrecords);
return cb(null, data);
}
});
diff --git a/lib/drivers/tandemDriver.js b/lib/drivers/tandemDriver.js
index c92b66614d..a00320216f 100644
--- a/lib/drivers/tandemDriver.js
+++ b/lib/drivers/tandemDriver.js
@@ -2049,7 +2049,8 @@ module.exports = function (config) {
}
}
simulator.finalBasal();
- data.post_records = [];
+
+ data.post_records = simulator.getEvents();
var sessionInfo = {
deviceTags: ['insulin-pump'],
@@ -2064,7 +2065,7 @@ module.exports = function (config) {
};
cfg.api.upload.toPlatform(
- simulator.getEvents(),
+ data.post_records,
sessionInfo,
progress,
cfg.groupId,
@@ -2076,7 +2077,6 @@ module.exports = function (config) {
return cb(err, data);
} else {
progress(100);
- data.post_records = data.post_records.concat(postrecords);
return cb(null, data);
}
}
diff --git a/lib/objectBuilder.js b/lib/objectBuilder.js
index 2f26b60f38..64afcb1cff 100644
--- a/lib/objectBuilder.js
+++ b/lib/objectBuilder.js
@@ -40,8 +40,6 @@
* == BSD2 LICENSE ==
*/
-'use strict';
-
var _ = require('lodash');
var util = require('util');
diff --git a/lib/redux/actions/async.js b/lib/redux/actions/async.js
new file mode 100644
index 0000000000..8adf424998
--- /dev/null
+++ b/lib/redux/actions/async.js
@@ -0,0 +1,521 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/* global chrome */
+
+import _ from 'lodash';
+import async from 'async';
+import semver from 'semver';
+
+import sundial from 'sundial';
+
+import * as actionTypes from '../constants/actionTypes';
+import * as actionSources from '../constants/actionSources';
+import { pages, paths, steps, urls } from '../constants/otherConstants';
+import { errorText } from '../utils/errors';
+
+import * as syncActions from './sync';
+import * as actionUtils from './utils';
+
+let services = {};
+let versionInfo = {};
+let daysForCareLink = null;
+
+/*
+ * ASYNCHRONOUS ACTION CREATORS
+ */
+
+export function doAppInit(opts, servicesToInit) {
+ return (dispatch, getState) => {
+ services = servicesToInit;
+ versionInfo.semver = opts.version;
+ versionInfo.name = opts.namedVersion;
+ daysForCareLink = opts.DEFAULT_CARELINK_DAYS;
+ const { api, carelink, device, localStore, log } = services;
+
+ dispatch(syncActions.initRequest());
+ dispatch(syncActions.hideUnavailableDevices(opts.os));
+
+ function initializeLocalStore(cb) {
+ log('Initializing local store.');
+ localStore.init(localStore.getInitialState(), () => { cb(); });
+ }
+ function initializeDevice(cb) {
+ log('Initializing device');
+ device.init({
+ api,
+ version: opts.namedVersion
+ }, cb);
+ }
+ function initializeCareLink(cb) {
+ log('Initializing CareLink');
+ carelink.init({ api }, cb);
+ }
+ function initializeApi(cb) {
+ log(`Initializing api`);
+ api.init(cb);
+ }
+ function setApiHosts(cb) {
+ log('Setting all api hosts');
+ api.setHosts(_.pick(opts, ['API_URL', 'UPLOAD_URL', 'BLIP_URL']));
+ dispatch(syncActions.setForgotPasswordUrl(api.makeBlipUrl(paths.FORGOT_PASSWORD)));
+ dispatch(syncActions.setSignUpUrl(api.makeBlipUrl(paths.SIGNUP)));
+ cb();
+ }
+
+ async.series([
+ initializeLocalStore,
+ initializeDevice,
+ initializeCareLink,
+ initializeApi,
+ setApiHosts
+ ], (err, results) => {
+ if (err) {
+ return dispatch(syncActions.initFailure(err));
+ }
+ let session = results[3];
+ if (session === undefined) {
+ dispatch(syncActions.setPage(pages.LOGIN));
+ dispatch(syncActions.initSuccess());
+ return dispatch(doVersionCheck());
+ }
+
+ async.series([
+ api.user.account,
+ api.user.profile,
+ api.user.getUploadGroups
+ ], (err, results) => {
+ if (err) {
+ return dispatch(syncActions.initFailure(err));
+ }
+ // remove env-switching context menu after login
+ if (typeof chrome !== 'undefined') {
+ services.log('Removing Chrome context menu');
+ chrome.contextMenus.removeAll();
+ }
+ dispatch(syncActions.initSuccess());
+ dispatch(doVersionCheck());
+ dispatch(syncActions.setUserInfoFromToken({
+ user: results[0],
+ profile: results[1],
+ memberships: results[2]
+ }));
+ const { uploadTargetUser } = getState();
+ if (uploadTargetUser !== null) {
+ dispatch(syncActions.setBlipViewDataUrl(
+ api.makeBlipUrl(actionUtils.viewDataPathForUser(uploadTargetUser))
+ ));
+ }
+ dispatch(retrieveTargetsFromStorage());
+ });
+ });
+ };
+}
+
+export function doLogin(creds, opts) {
+ return (dispatch, getState) => {
+ const { api } = services;
+ dispatch(syncActions.loginRequest());
+
+ async.series([
+ api.user.login.bind(null, creds, opts),
+ api.user.profile,
+ api.user.getUploadGroups
+ ], (err, results) => {
+ if (err) {
+ return dispatch(syncActions.loginFailure(err.status));
+ }
+ // remove env-switching context menu after login
+ if (typeof chrome !== 'undefined') {
+ services.log('Removing Chrome context menu');
+ chrome.contextMenus.removeAll();
+ }
+ dispatch(syncActions.loginSuccess({
+ user: results[0].user,
+ profile: results[1],
+ memberships: results[2]
+ }));
+ const { uploadTargetUser } = getState();
+ if (uploadTargetUser !== null) {
+ dispatch(syncActions.setBlipViewDataUrl(
+ api.makeBlipUrl(actionUtils.viewDataPathForUser(uploadTargetUser))
+ ));
+ }
+ dispatch(retrieveTargetsFromStorage());
+ });
+ };
+}
+
+export function doLogout() {
+ return (dispatch) => {
+ const { api } = services;
+ dispatch(syncActions.logoutRequest());
+ api.user.logout((err) => {
+ if (err) {
+ dispatch(syncActions.logoutFailure());
+ dispatch(syncActions.setPage(pages.LOGIN, actionSources.USER));
+ }
+ else {
+ dispatch(syncActions.logoutSuccess());
+ dispatch(syncActions.setPage(pages.LOGIN, actionSources.USER));
+ }
+ });
+ };
+}
+
+export function doCareLinkUpload(deviceKey, creds, utc) {
+ return (dispatch, getState) => {
+ const { api, carelink } = services;
+ const version = versionInfo.semver;
+ const { devices, targetTimezones, uploadTargetUser } = getState();
+
+ const targetDevice = devices[deviceKey];
+
+ dispatch(syncActions.fetchCareLinkRequest(uploadTargetUser, deviceKey));
+
+ api.upload.fetchCarelinkData({
+ carelinkUsername: creds.username,
+ carelinkPassword: creds.password,
+ daysAgo: daysForCareLink,
+ targetUserId: uploadTargetUser
+ }, (err, data) => {
+ if (err) {
+ let fetchErr = new Error(errorText.E_FETCH_CARELINK);
+ let fetchErrProps = {
+ details: err.message,
+ utc: actionUtils.getUtc(utc),
+ code: 'E_FETCH_CARELINK',
+ version: version
+ };
+ dispatch(syncActions.fetchCareLinkFailure(errorText.E_FETCH_CARELINK));
+ return dispatch(syncActions.uploadFailure(fetchErr, fetchErrProps, targetDevice));
+ }
+ if (data.search(/302 Moved Temporarily/) !== -1) {
+ let credsErr = new Error(errorText.E_CARELINK_CREDS);
+ let credsErrProps = {
+ utc: actionUtils.getUtc(utc),
+ code: 'E_CARELINK_CREDS',
+ version: version
+ };
+ dispatch(syncActions.fetchCareLinkFailure(errorText.E_CARELINK_CREDS));
+ return dispatch(syncActions.uploadFailure(credsErr, credsErrProps, targetDevice));
+ }
+ dispatch(syncActions.fetchCareLinkSuccess(uploadTargetUser, deviceKey));
+
+ const opts = {
+ targetId: uploadTargetUser,
+ timezone: targetTimezones[uploadTargetUser],
+ progress: actionUtils.makeProgressFn(dispatch),
+ version: version
+ };
+ carelink.upload(data, opts, actionUtils.makeUploadCb(dispatch, getState, 'E_CARELINK_UPLOAD', utc));
+ });
+ };
+}
+
+export function doDeviceUpload(driverId, utc) {
+ return (dispatch, getState) => {
+ const { device } = services;
+ const version = versionInfo.semver;
+ const { devices, os, targetTimezones, uploadTargetUser } = getState();
+ const targetDevice = _.findWhere(devices, {source: {driverId: driverId}});
+ dispatch(syncActions.deviceDetectRequest());
+ const opts = {
+ targetId: uploadTargetUser,
+ timezone: targetTimezones[uploadTargetUser],
+ progress: actionUtils.makeProgressFn(dispatch),
+ version: version
+ };
+ const { uploadsByUser } = getState();
+ const currentUpload = _.get(
+ uploadsByUser,
+ [uploadTargetUser, targetDevice.key],
+ {}
+ );
+ if (currentUpload.file) {
+ opts.filedata = currentUpload.file.data;
+ opts.filename = currentUpload.file.name;
+ }
+
+ device.detect(driverId, opts, (err, dev) => {
+ if (err) {
+ if ((os === 'mac' && _.get(targetDevice, ['showDriverLink', os], false) === true) ||
+ (os === 'win' && _.get(targetDevice, ['showDriverLink', os], false) === true)) {
+ let displayErr = new Error();
+ let driverLinkErrProps = {
+ details: err.message,
+ utc: actionUtils.getUtc(utc),
+ code: 'E_DRIVER',
+ version: version
+ };
+ displayErr.driverLink = urls.DRIVER_DOWNLOAD;
+ return dispatch(syncActions.uploadFailure(displayErr, driverLinkErrProps, targetDevice));
+ }
+ else {
+ let displayErr = new Error(errorText.E_SERIAL_CONNECTION);
+ let deviceDetectErrProps = {
+ details: err.message,
+ utc: actionUtils.getUtc(utc),
+ code: 'E_SERIAL_CONNECTION',
+ version: version
+ };
+ return dispatch(syncActions.uploadFailure(displayErr, deviceDetectErrProps, targetDevice));
+ }
+ }
+
+ if (!dev && opts.filename == null) {
+ let displayErr = new Error(errorText.E_HID_CONNECTION);
+ let disconnectedErrProps = {
+ utc: actionUtils.getUtc(utc),
+ code: 'E_HID_CONNECTION',
+ version: version
+ };
+ return dispatch(syncActions.uploadFailure(displayErr, disconnectedErrProps, targetDevice));
+ }
+
+ device.upload(driverId, opts, actionUtils.makeUploadCb(dispatch, getState, 'E_DEVICE_UPLOAD', utc));
+ });
+ };
+}
+
+export function doUpload(deviceKey, opts, utc) {
+ return (dispatch, getState) => {
+ dispatch(syncActions.versionCheckRequest());
+ const { api } = services;
+ const version = versionInfo.semver;
+ api.upload.getVersions((err, versions) => {
+ if (err) {
+ dispatch(syncActions.versionCheckFailure(err));
+ return dispatch(syncActions.uploadAborted());
+ }
+ const { uploaderMinimum } = versions;
+ // if either the version from the jellyfish response
+ // or the local uploader version is somehow an invalid semver
+ // we will catch the error and dispatch versionCheckFailure
+ try {
+ const upToDate = semver.gte(version, uploaderMinimum);
+ if (!upToDate) {
+ dispatch(syncActions.versionCheckFailure(null, version, uploaderMinimum));
+ return dispatch(syncActions.uploadAborted());
+ }
+ else {
+ dispatch(syncActions.versionCheckSuccess());
+ const { devices, uploadTargetUser, working } = getState();
+ if (working.uploading === true) {
+ return dispatch(syncActions.uploadAborted());
+ }
+
+ dispatch(syncActions.uploadRequest(uploadTargetUser, devices[deviceKey], utc));
+
+ const targetDevice = devices[deviceKey];
+ const deviceType = targetDevice.source.type;
+
+ if (_.includes(['device', 'block'], deviceType)) {
+ dispatch(doDeviceUpload(targetDevice.source.driverId, utc));
+ }
+ else if (deviceType === 'carelink') {
+ dispatch(doCareLinkUpload(deviceKey, opts, utc));
+ }
+ }
+ }
+ catch(err) {
+ dispatch(syncActions.versionCheckFailure(err));
+ return dispatch(syncActions.uploadAborted());
+ }
+ });
+ };
+}
+
+export function readFile(userId, deviceKey, file, extension) {
+ return (dispatch, getState) => {
+ if (!file) {
+ return;
+ }
+ dispatch(syncActions.choosingFile(userId, deviceKey));
+ const version = versionInfo.semver;
+
+ if (file.name.slice(-extension.length) !== extension) {
+ let err = new Error(errorText.E_FILE_EXT + extension);
+ let errProps = {
+ code: 'E_FILE_EXT',
+ version: version
+ };
+ return dispatch(syncActions.readFileAborted(err, errProps));
+ }
+ else {
+ let reader = new FileReader();
+ reader.onloadstart = () => {
+ dispatch(syncActions.readFileRequest(userId, deviceKey, file.name));
+ };
+
+ reader.onerror = () => {
+ let err = new Error(errorText.E_READ_FILE + file.name);
+ let errProps = {
+ code: 'E_READ_FILE',
+ version: version
+ };
+ return dispatch(syncActions.readFileFailure(err, errProps));
+ };
+
+ reader.onloadend = ((theFile) => {
+ return (e) => {
+ dispatch(syncActions.readFileSuccess(userId, deviceKey, e.srcElement.result));
+ dispatch(doUpload(deviceKey));
+ };
+ })(file);
+
+ reader.readAsArrayBuffer(file);
+ }
+ };
+}
+
+export function doVersionCheck() {
+ return (dispatch, getState) => {
+ dispatch(syncActions.versionCheckRequest());
+ const { api } = services;
+ const version = versionInfo.semver;
+ api.upload.getVersions((err, versions) => {
+ if (err) {
+ return dispatch(syncActions.versionCheckFailure(err));
+ }
+ const { uploaderMinimum } = versions;
+ // if either the version from the jellyfish response
+ // or the local uploader version is somehow an invalid semver
+ // we will catch the error and dispatch versionCheckFailure
+ try {
+ const upToDate = semver.gte(version, uploaderMinimum);
+ if (!upToDate) {
+ return dispatch(syncActions.versionCheckFailure(null, version, uploaderMinimum));
+ }
+ else {
+ return dispatch(syncActions.versionCheckSuccess());
+ }
+ }
+ catch(err) {
+ return dispatch(syncActions.versionCheckFailure(err));
+ }
+ });
+ };
+}
+
+/*
+ * COMPLEX ACTION CREATORS
+ */
+
+export function putTargetsInStorage() {
+ return (dispatch, getState) => {
+ const { targetDevices, targetTimezones, uploadTargetUser } = getState();
+ let usersWithTargets = {};
+ _.forOwn(targetDevices, (devicesArray, userId) => {
+ usersWithTargets[userId] = _.map(devicesArray, (deviceKey) => {
+ return {key: deviceKey};
+ });
+ });
+ _.forOwn(targetTimezones, (timezoneName, userId) => {
+ usersWithTargets[userId] = _.map(usersWithTargets[userId], (target) => {
+ if (timezoneName != null) {
+ target.timezone = timezoneName;
+ }
+ return target;
+ });
+ });
+ const { localStore } = services;
+ dispatch(syncActions.retrieveUsersTargetsFromStorage());
+ const devicesInStorage = localStore.getItem('devices') || {};
+ dispatch(syncActions.putUsersTargetsInStorage());
+ localStore.setItem(
+ 'devices',
+ Object.assign({}, devicesInStorage, usersWithTargets)
+ );
+
+ if (!_.isEmpty(targetTimezones[uploadTargetUser]) &&
+ !_.isEmpty(targetDevices[uploadTargetUser])) {
+ dispatch(syncActions.setPage(pages.MAIN));
+ }
+
+ const uploadsByUser = actionUtils.getDeviceTargetsByUser(usersWithTargets);
+
+ dispatch(syncActions.setUploads(uploadsByUser));
+ };
+}
+
+export function retrieveTargetsFromStorage() {
+ return (dispatch, getState) => {
+ const { devices, uploadTargetUser } = getState();
+ const { api, localStore } = services;
+ dispatch(syncActions.retrieveUsersTargetsFromStorage());
+ const targets = localStore.getItem('devices');
+ if (targets === null) {
+ return dispatch(syncActions.setPage(pages.SETTINGS));
+ }
+ else {
+ const uploadsByUser = actionUtils.getDeviceTargetsByUser(targets);
+
+ dispatch(syncActions.setUploads(uploadsByUser));
+ }
+ dispatch(syncActions.setUsersTargets(targets));
+
+ if (targets[uploadTargetUser] != null) {
+ const userTargets = targets[uploadTargetUser];
+ const targetDeviceKeys = _.pluck(userTargets, 'key');
+ const supportedDeviceKeys = Object.keys(devices);
+ const atLeastOneDeviceSupportedOnSystem = _.some(targetDeviceKeys, (key) => {
+ return _.includes(supportedDeviceKeys, key);
+ });
+ let timezones = [];
+ _.each(userTargets, (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 (dispatch, getState) => {
+ const { devices, targetDevices, targetTimezones } = getState();
+ dispatch(syncActions.setUploadTargetUser(targetId));
+ const { api } = services;
+ dispatch(syncActions.setBlipViewDataUrl(
+ api.makeBlipUrl(actionUtils.viewDataPathForUser(targetId))
+ ));
+ const targetedDevices = _.get(targetDevices, targetId, []);
+ const targetedTimezone = _.get(targetTimezones, targetId, null);
+ const supportedDeviceKeys = Object.keys(devices);
+ const atLeastOneDeviceSupportedOnSystem = _.some(targetedDevices, (key) => {
+ return _.includes(supportedDeviceKeys, key);
+ });
+ if (_.isEmpty(targetedDevices) || _.isEmpty(targetedTimezone)) {
+ return dispatch(syncActions.setPage(pages.SETTINGS));
+ }
+ if (!atLeastOneDeviceSupportedOnSystem) {
+ return dispatch(syncActions.setPage(pages.SETTINGS));
+ }
+ };
+}
diff --git a/lib/redux/actions/index.js b/lib/redux/actions/index.js
new file mode 100644
index 0000000000..859a62bf83
--- /dev/null
+++ b/lib/redux/actions/index.js
@@ -0,0 +1,24 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import * as async from './async';
+import * as sync from './sync';
+
+export default {
+ async: async,
+ sync: sync
+};
diff --git a/lib/redux/actions/sync.js b/lib/redux/actions/sync.js
new file mode 100644
index 0000000000..97f01b38fe
--- /dev/null
+++ b/lib/redux/actions/sync.js
@@ -0,0 +1,484 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+
+import sundial from 'sundial';
+
+import * as actionTypes from '../constants/actionTypes';
+import * as actionSources from '../constants/actionSources';
+import * as metrics from '../constants/metrics';
+import { pages, paths, steps } from '../constants/otherConstants';
+
+import { addInfoToError, errorText, getAppInitErrorMessage, getLoginErrorMessage, getLogoutErrorMessage, UnsupportedError } from '../utils/errors';
+
+import * as actionUtils from './utils';
+
+export function addTargetDevice(userId, deviceKey) {
+ return {
+ type: actionTypes.ADD_TARGET_DEVICE,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.ADD_TARGET_DEVICE]}
+ };
+}
+
+// NB: this action exists purely to trigger the metrics middleware
+// no reducer responds to it to adjust any state!
+export function clickGoToBlip() {
+ return {
+ type: actionTypes.CLICK_GO_TO_BLIP,
+ meta: {
+ source: actionSources[actionTypes.CLICK_GO_TO_BLIP],
+ metric: {eventName: metrics.CLICK_GO_TO_BLIP}
+ }
+ };
+}
+
+export function hideUnavailableDevices(os) {
+ return {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: { os },
+ meta: {source: actionSources[actionTypes.HIDE_UNAVAILABLE_DEVICES]}
+ };
+}
+
+export function removeTargetDevice(userId, deviceKey) {
+ return {
+ type: actionTypes.REMOVE_TARGET_DEVICE,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.REMOVE_TARGET_DEVICE]}
+ };
+}
+
+export function resetUpload(userId, deviceKey) {
+ return {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.RESET_UPLOAD]}
+ };
+}
+
+export function setBlipViewDataUrl(url) {
+ return {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ };
+}
+
+export function setForgotPasswordUrl(url) {
+ return {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_FORGOT_PASSWORD_URL]}
+ };
+}
+
+export function setPage(page, actionSource = actionSources[actionTypes.SET_PAGE]) {
+ return {
+ type: actionTypes.SET_PAGE,
+ payload: { page },
+ meta: {source: actionSource}
+ };
+}
+
+export function setSignUpUrl(url) {
+ return {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_SIGNUP_URL]}
+ };
+}
+
+export function setTargetTimezone(userId, timezoneName) {
+ return {
+ type: actionTypes.SET_TARGET_TIMEZONE,
+ payload: { userId, timezoneName },
+ meta: {source: actionSources[actionTypes.SET_TARGET_TIMEZONE]}
+ };
+}
+
+export function setUploads(devicesByUser) {
+ return {
+ type: actionTypes.SET_UPLOADS,
+ payload: { devicesByUser },
+ meta: {source: actionSources[actionTypes.SET_UPLOADS]}
+ };
+}
+
+export function setUploadTargetUser(userId) {
+ return {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: { userId },
+ meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]}
+ };
+}
+
+export function toggleDropdown(previous, actionSource = actionSources[actionTypes.TOGGLE_DROPDOWN]) {
+ return {
+ type: actionTypes.TOGGLE_DROPDOWN,
+ payload: { isVisible: !previous },
+ meta: {source: actionSource}
+ };
+}
+
+export function toggleErrorDetails(userId, deviceKey, previous) {
+ if (_.includes([null, undefined], previous)) {
+ previous = false;
+ }
+ return {
+ type: actionTypes.TOGGLE_ERROR_DETAILS,
+ payload: { isVisible: !previous, userId, deviceKey },
+ meta: {source: actionSources[actionTypes.TOGGLE_ERROR_DETAILS]}
+ };
+}
+
+/*
+ * relating to async action creator doAppInit
+ */
+
+export function initRequest() {
+ return {
+ type: actionTypes.INIT_APP_REQUEST,
+ meta: {source: actionSources[actionTypes.INIT_APP_REQUEST]}
+ };
+}
+
+export function initSuccess(session) {
+ return {
+ type: actionTypes.INIT_APP_SUCCESS,
+ meta: {source: actionSources[actionTypes.INIT_APP_SUCCESS]}
+ };
+}
+
+export function initFailure(err) {
+ return {
+ type: actionTypes.INIT_APP_FAILURE,
+ error: true,
+ payload: new Error(getAppInitErrorMessage(err.status || null)),
+ meta: {source: actionSources[actionTypes.INIT_APP_FAILURE]}
+ };
+}
+
+export function setUserInfoFromToken(results) {
+ const { user, profile, memberships } = results;
+ return {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships },
+ meta: {source: actionSources[actionTypes.SET_USER_INFO_FROM_TOKEN]}
+ };
+}
+
+/*
+ * relating to async action creator doLogin
+ */
+
+export function loginRequest() {
+ return {
+ type: actionTypes.LOGIN_REQUEST,
+ meta: {source: actionSources[actionTypes.LOGIN_REQUEST]}
+ };
+}
+
+export function loginSuccess(results) {
+ const { user, profile, memberships } = results;
+ return {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships },
+ meta: {
+ source: actionSources[actionTypes.LOGIN_SUCCESS],
+ metric: {eventName: metrics.LOGIN_SUCCESS}
+ }
+ };
+}
+
+export function loginFailure(errorCode) {
+ return {
+ type: actionTypes.LOGIN_FAILURE,
+ error: true,
+ payload: new Error(getLoginErrorMessage(errorCode)),
+ meta: {source: actionSources[actionTypes.LOGIN_FAILURE]}
+ };
+}
+
+/*
+ * relating to async action creator doLogout
+ */
+
+export function logoutRequest() {
+ return {
+ type: actionTypes.LOGOUT_REQUEST,
+ meta: {
+ source: actionSources[actionTypes.LOGOUT_REQUEST],
+ metric: {eventName: metrics.LOGOUT_REQUEST}
+ }
+ };
+}
+
+export function logoutSuccess() {
+ return {
+ type: actionTypes.LOGOUT_SUCCESS,
+ meta: {source: actionSources[actionTypes.LOGOUT_SUCCESS]}
+ };
+}
+
+export function logoutFailure() {
+ return {
+ type: actionTypes.LOGOUT_FAILURE,
+ error: true,
+ payload: new Error(getLogoutErrorMessage()),
+ meta: {source: actionSources[actionTypes.LOGOUT_FAILURE]}
+ };
+}
+
+/*
+ * relating to async action creator doCareLinkUpload
+ */
+
+export function fetchCareLinkRequest(userId, deviceKey) {
+ return {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ };
+}
+
+export function fetchCareLinkSuccess(userId, deviceKey) {
+ return {
+ type: actionTypes.CARELINK_FETCH_SUCCESS,
+ payload: { userId, deviceKey },
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_SUCCESS],
+ metric: {eventName: metrics.CARELINK_FETCH_SUCCESS}
+ }
+ };
+}
+
+export function fetchCareLinkFailure(message) {
+ return {
+ type: actionTypes.CARELINK_FETCH_FAILURE,
+ error: true,
+ payload: new Error(message),
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_FAILURE],
+ metric: {eventName: metrics.CARELINK_FETCH_FAILURE}
+ }
+ };
+}
+
+/*
+ * relating to async action creator doUpload
+ */
+
+export function uploadAborted() {
+ return {
+ type: actionTypes.UPLOAD_ABORTED,
+ error: true,
+ payload: new Error(errorText.E_UPLOAD_IN_PROGRESS),
+ meta: {source: actionSources[actionTypes.UPLOAD_ABORTED]}
+ };
+}
+
+export function uploadRequest(userId, device, utc) {
+ utc = actionUtils.getUtc(utc);
+ return {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey: device.key, utc },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: `${metrics.UPLOAD_REQUEST} ${actionUtils.getUploadTrackingId(device)}`,
+ properties: {
+ type: _.get(device, 'source.type', undefined),
+ source: _.get(device, 'source.driverId', undefined)
+ }
+ }
+ }
+ };
+}
+
+export function uploadProgress(step, percentage) {
+ return {
+ type: actionTypes.UPLOAD_PROGRESS,
+ payload: { step, percentage },
+ meta: {source: actionSources[actionTypes.UPLOAD_PROGRESS]}
+ };
+}
+
+export function uploadSuccess(userId, device, upload, data, utc) {
+ utc = actionUtils.getUtc(utc);
+ return {
+ type: actionTypes.UPLOAD_SUCCESS,
+ payload: { userId, deviceKey: device.key, data, utc },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_SUCCESS],
+ metric: {
+ eventName: `${metrics.UPLOAD_SUCCESS} ${actionUtils.getUploadTrackingId(device)}`,
+ properties: {
+ type: _.get(device, 'source.type', undefined),
+ source: _.get(device, 'source.driverId', undefined),
+ started: upload.history[0].start || '',
+ finished: utc || '',
+ processed: data.length || 0
+ }
+ }
+ }
+ };
+}
+
+export function uploadFailure(err, errProps, device) {
+ err = addInfoToError(err, errProps);
+ return {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: `${metrics.UPLOAD_FAILURE} ${actionUtils.getUploadTrackingId(device)}`,
+ properties: {
+ type: _.get(device, 'source.type', undefined),
+ source: _.get(device, 'source.driverId', undefined),
+ error: err
+ }
+ }
+ }
+ };
+}
+
+export function deviceDetectRequest() {
+ return {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ };
+}
+
+/*
+ * relating to async action creator readFile
+ */
+
+export function choosingFile(userId, deviceKey) {
+ return {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CHOOSING_FILE]}
+ };
+}
+
+export function readFileAborted(err, errProps) {
+ return {
+ type: actionTypes.READ_FILE_ABORTED,
+ error: true,
+ payload: addInfoToError(err, errProps),
+ meta: {source: actionSources[actionTypes.READ_FILE_ABORTED]}
+ };
+}
+
+export function readFileRequest(userId, deviceKey, filename) {
+ return {
+ type: actionTypes.READ_FILE_REQUEST,
+ payload: { userId, deviceKey, filename },
+ meta: {source: actionSources[actionTypes.READ_FILE_REQUEST]}
+ };
+}
+
+export function readFileSuccess(userId, deviceKey, filedata) {
+ return {
+ type: actionTypes.READ_FILE_SUCCESS,
+ payload: { userId, deviceKey, filedata },
+ meta: {source: actionSources[actionTypes.READ_FILE_SUCCESS]}
+ };
+}
+
+export function readFileFailure(err, errProps) {
+ return {
+ type: actionTypes.READ_FILE_FAILURE,
+ error: true,
+ payload: addInfoToError(err, errProps),
+ meta: {source: actionSources[actionTypes.READ_FILE_FAILURE]}
+ };
+}
+
+/*
+ * relating to async action creator doVersionCheck
+ */
+
+export function versionCheckRequest() {
+ return {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ };
+}
+
+export function versionCheckSuccess() {
+ return {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ };
+}
+
+export function versionCheckFailure(err, currentVersion, requiredVersion) {
+ if (err != null) {
+ return {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_FAILURE]}
+ };
+ }
+ else {
+ return {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: new UnsupportedError(currentVersion, requiredVersion),
+ meta: {
+ source: actionSources[actionTypes.VERSION_CHECK_FAILURE],
+ metric: {
+ eventName: metrics.VERSION_CHECK_FAILURE_OUTDATED,
+ properties: { requiredVersion }
+ }
+ }
+ };
+ }
+}
+
+/*
+ * relating to side-effect-performing action creators
+ * retrieveTargetsFromStorage and putTargetsInStorage
+ */
+
+export function putUsersTargetsInStorage() {
+ return {
+ type: actionTypes.STORING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.STORING_USERS_TARGETS]}
+ };
+}
+
+export function retrieveUsersTargetsFromStorage() {
+ return {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ };
+}
+
+export function setUsersTargets(targets) {
+ return {
+ type: actionTypes.SET_USERS_TARGETS,
+ payload: { targets },
+ meta: {source: actionSources[actionTypes.SET_USERS_TARGETS]}
+ };
+}
diff --git a/lib/redux/actions/utils.js b/lib/redux/actions/utils.js
new file mode 100644
index 0000000000..bb7c1fb09b
--- /dev/null
+++ b/lib/redux/actions/utils.js
@@ -0,0 +1,95 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/* global __TEST__ */
+
+import _ from 'lodash';
+import stacktrace from 'stack-trace';
+
+import sundial from 'sundial';
+
+import { errorText } from '../utils/errors';
+import * as syncActions from './sync';
+
+export function getDeviceTargetsByUser(targetsByUser) {
+ return _.mapValues(targetsByUser, (targets) => {
+ return _.pluck(targets, 'key');
+ });
+}
+
+export function getUploadTrackingId(device) {
+ const source = device.source;
+ if (source.type === 'device' || source.type === 'block') {
+ return source.driverId;
+ }
+ if (source.type === 'carelink') {
+ return 'CareLink';
+ }
+ return null;
+}
+
+export function getUtc(utc) {
+ return _.isEmpty(utc) ? sundial.utcDateString() : utc;
+}
+
+export function makeProgressFn(dispatch, step, percentage) {
+ return (step, percentage) => {
+ dispatch(syncActions.uploadProgress(step, percentage));
+ };
+}
+
+export function makeUploadCb(dispatch, getState, errCode, utc) {
+ return (err, recs) => {
+ const { devices, uploadsByUser, uploadTargetDevice, uploadTargetUser, version } = getState();
+ const targetDevice = devices[uploadTargetDevice];
+ if (err) {
+ // the drivers sometimes just pass a string arg as err, instead of an actual error :/
+ if (typeof err === 'string') {
+ err = new Error(err);
+ }
+ const serverErr = 'Origin is not allowed by Access-Control-Allow-Origin';
+ let displayErr = new Error(err.message === serverErr ?
+ errorText.E_SERVER_ERR : errorText[errCode]);
+ let uploadErrProps = {
+ details: err.message,
+ utc: getUtc(utc),
+ name: err.name || 'Uncaught or API POST error',
+ step: err.step || null,
+ code: errCode,
+ version: version
+ };
+
+ if (!__TEST__) {
+ uploadErrProps.stringifiedStack = _.pluck(
+ _.filter(
+ stacktrace.parse(err),
+ (cs) => { return cs.functionName !== null; }
+ ),
+ 'functionName'
+ ).join(', ');
+ }
+ return dispatch(syncActions.uploadFailure(displayErr, uploadErrProps, targetDevice));
+ }
+ const currentUpload = _.get(uploadsByUser, [uploadTargetUser, targetDevice.key], {});
+ dispatch(syncActions.uploadSuccess(uploadTargetUser, targetDevice, currentUpload, recs, utc));
+
+ };
+}
+
+export function viewDataPathForUser(uploadTargetUser) {
+ return `/patients/${uploadTargetUser}/data`;
+}
diff --git a/lib/redux/constants/actionSources.js b/lib/redux/constants/actionSources.js
new file mode 100644
index 0000000000..9a63403e88
--- /dev/null
+++ b/lib/redux/constants/actionSources.js
@@ -0,0 +1,94 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+export const USER = 'USER';
+export const USER_VISIBLE = 'USER_VISIBLE';
+export const UNDER_THE_HOOD = 'UNDER_THE_HOOD';
+
+/**
+ * Syncronous action types
+ */
+export const ADD_TARGET_DEVICE = USER;
+export const CLICK_GO_TO_BLIP = USER;
+export const HIDE_UNAVAILABLE_DEVICES = USER_VISIBLE;
+export const REMOVE_TARGET_DEVICE = USER;
+export const RESET_UPLOAD = USER;
+export const RETRIEVING_USERS_TARGETS = UNDER_THE_HOOD;
+export const SET_BLIP_VIEW_DATA_URL = USER_VISIBLE;
+export const SET_DEFAULT_TARGET_ID = USER_VISIBLE;
+export const SET_FORGOT_PASSWORD_URL = USER_VISIBLE;
+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;
+export const SET_VERSION = USER_VISIBLE;
+export const STORING_USERS_TARGETS = UNDER_THE_HOOD;
+export const TOGGLE_DROPDOWN = USER;
+export const TOGGLE_ERROR_DETAILS = USER;
+
+/*
+ * Asyncronous action types
+ */
+
+export const INIT_APP_REQUEST = UNDER_THE_HOOD;
+export const INIT_APP_SUCCESS = UNDER_THE_HOOD;
+export const INIT_APP_FAILURE = USER_VISIBLE;
+
+// user.login
+export const LOGIN_REQUEST = USER;
+export const LOGIN_SUCCESS = USER_VISIBLE;
+export const LOGIN_FAILURE = USER_VISIBLE;
+
+// user.logout
+export const LOGOUT_REQUEST = USER;
+export const LOGOUT_SUCCESS = USER_VISIBLE;
+// because we don't surface logout errors in the UI
+export const LOGOUT_FAILURE = UNDER_THE_HOOD;
+
+// uploading devices
+export const UPLOAD_REQUEST = USER;
+export const UPLOAD_PROGRESS = USER_VISIBLE;
+export const UPLOAD_SUCCESS = USER_VISIBLE;
+export const UPLOAD_FAILURE = USER_VISIBLE;
+export const UPLOAD_ABORTED = USER_VISIBLE;
+
+export const CARELINK_FETCH_REQUEST = USER;
+export const CARELINK_FETCH_SUCCESS = USER_VISIBLE;
+export const CARELINK_FETCH_FAILURE = USER_VISIBLE;
+
+export const CARELINK_UPLOAD_REQUEST = UNDER_THE_HOOD;
+export const CARELINK_UPLOAD_SUCCESS = USER_VISIBLE;
+export const CARELINK_UPLOAD_FAILURE = USER_VISIBLE;
+
+export const DEVICE_DETECT_REQUEST = UNDER_THE_HOOD;
+export const DEVICE_DETECT_FAILURE = USER_VISIBLE;
+export const DEVICE_DETECT_SUCCESS = UNDER_THE_HOOD;
+
+export const READ_FILE_REQUEST = USER;
+export const READ_FILE_SUCCESS = USER_VISIBLE;
+export const READ_FILE_FAILURE = USER_VISIBLE;
+export const READ_FILE_ABORTED = USER_VISIBLE;
+export const CHOOSING_FILE = USER;
+
+// version check
+export const VERSION_CHECK_REQUEST = UNDER_THE_HOOD;
+export const VERSION_CHECK_SUCCESS = UNDER_THE_HOOD;
+export const VERSION_CHECK_FAILURE = USER_VISIBLE;
diff --git a/lib/redux/constants/actionTypes.js b/lib/redux/constants/actionTypes.js
new file mode 100644
index 0000000000..649d77a82f
--- /dev/null
+++ b/lib/redux/constants/actionTypes.js
@@ -0,0 +1,89 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/**
+ * Syncronous action types
+ */
+export const ADD_TARGET_DEVICE = 'ADD_TARGET_DEVICE';
+export const CLICK_GO_TO_BLIP = 'CLICK_GO_TO_BLIP';
+export const HIDE_UNAVAILABLE_DEVICES = 'HIDE_UNAVAILABLE_DEVICES';
+export const REMOVE_TARGET_DEVICE = 'REMOVE_TARGET_DEVICE';
+export const RESET_UPLOAD = 'RESET_UPLOAD';
+export const RETRIEVING_USERS_TARGETS = 'RETRIEVING_USERS_TARGETS';
+export const SET_BLIP_VIEW_DATA_URL = 'SET_BLIP_VIEW_DATA_URL';
+export const SET_DEFAULT_TARGET_ID = 'SET_DEFAULT_TARGET_ID';
+export const SET_FORGOT_PASSWORD_URL = 'SET_FORGOT_PASSWORD_URL';
+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';
+export const SET_VERSION = 'SET_VERSION';
+export const STORING_USERS_TARGETS = 'STORING_USERS_TARGETS';
+export const TOGGLE_DROPDOWN = 'TOGGLE_DROPDOWN';
+export const TOGGLE_ERROR_DETAILS = 'TOGGLE_ERROR_DETAILS';
+
+/*
+ * Asyncronous action types
+ */
+
+export const INIT_APP_REQUEST = 'INIT_APP_REQUEST';
+export const INIT_APP_SUCCESS = 'INIT_APP_SUCCESS';
+export const INIT_APP_FAILURE = 'INIT_APP_FAILURE';
+
+// user.login
+export const LOGIN_REQUEST = 'LOGIN_REQUEST';
+export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+export const LOGIN_FAILURE = 'LOGIN_FAILURE';
+
+// user.logout
+export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
+export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
+export const LOGOUT_FAILURE = 'LOGIN_FAILURE';
+
+// uploading devices
+export const UPLOAD_REQUEST = 'UPLOAD_REQUEST';
+export const UPLOAD_PROGRESS = 'UPLOAD_PROGRESS';
+export const UPLOAD_SUCCESS = 'UPLOAD_SUCCESS';
+export const UPLOAD_FAILURE = 'UPLOAD_FAILURE';
+export const UPLOAD_ABORTED = 'UPLOAD_ABORTED';
+
+export const CARELINK_FETCH_REQUEST = 'CARELINK_FETCH_REQUEST';
+export const CARELINK_FETCH_SUCCESS = 'CARELINK_FETCH_SUCCESS';
+export const CARELINK_FETCH_FAILURE = 'CARELINK_FETCH_FAILURE';
+
+export const CARELINK_UPLOAD_REQUEST = 'CARELINK_UPLOAD_REQUEST';
+export const CARELINK_UPLOAD_SUCCESS = 'CARELINK_UPLOAD_SUCCESS';
+export const CARELINK_UPLOAD_FAILURE = 'CARELINK_UPLOAD_FAILURE';
+
+export const DEVICE_DETECT_REQUEST = 'DEVICE_DETECT_REQUEST';
+export const DEVICE_DETECT_SUCCESS = 'DEVICE_DETECT_SUCCESS';
+export const DEVICE_DETECT_FAILURE = 'DEVICE_DETECT_FAILURE';
+
+export const READ_FILE_REQUEST = 'READ_FILE_REQUEST';
+export const READ_FILE_SUCCESS = 'READ_FILE_SUCCESS';
+export const READ_FILE_FAILURE = 'READ_FILE_FAILURE';
+export const READ_FILE_ABORTED = 'READ_FILE_ABORTED';
+export const CHOOSING_FILE = 'CHOOSING_FILE';
+
+// version check
+export const VERSION_CHECK_REQUEST = 'VERSION_CHECK_REQUEST';
+export const VERSION_CHECK_SUCCESS = 'VERSION_CHECK_SUCCESS';
+export const VERSION_CHECK_FAILURE = 'VERSION_CHECK_FAILURE';
diff --git a/lib/redux/constants/metrics.js b/lib/redux/constants/metrics.js
new file mode 100644
index 0000000000..a86cd7b076
--- /dev/null
+++ b/lib/redux/constants/metrics.js
@@ -0,0 +1,30 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+export const CLICK_GO_TO_BLIP = 'Clicked See Data in Blip';
+
+export const LOGIN_SUCCESS = 'Login Successful';
+export const LOGOUT_REQUEST = 'Clicked Log Out';
+
+export const UPLOAD_REQUEST = 'Upload Attempted';
+export const UPLOAD_SUCCESS = 'Upload Successful';
+export const UPLOAD_FAILURE = 'Upload Failed';
+
+export const CARELINK_FETCH_SUCCESS = 'CareLink Fetch Successful';
+export const CARELINK_FETCH_FAILURE = 'CareLink Fetch Failed';
+
+export const VERSION_CHECK_FAILURE_OUTDATED = '(Partial) Uploader Version No Longer Supported';
diff --git a/lib/redux/constants/otherConstants.js b/lib/redux/constants/otherConstants.js
new file mode 100644
index 0000000000..a84e349c38
--- /dev/null
+++ b/lib/redux/constants/otherConstants.js
@@ -0,0 +1,48 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+export const pages = {
+ LOADING: 'LOADING',
+ LOGIN: 'LOGIN',
+ MAIN: 'MAIN',
+ SETTINGS: 'SETTINGS'
+};
+
+export const paths = {
+ FORGOT_PASSWORD: '/request-password-from-uploader',
+ SIGNUP: '/signup'
+};
+
+export const steps = {
+ start: 'START',
+ carelinkFetch: 'CARELINK_FETCH',
+ choosingFile: 'CHOOSING_FILE',
+ detect: 'DETECT',
+ setup: 'SETUP',
+ connect: 'CONNECT',
+ getConfigInfo: 'GET_CONFIG_INFO',
+ fetchData: 'FETCH_DATA',
+ processData: 'PROCESS_DATA',
+ uploadData: 'UPLOAD_DATA',
+ disconnect: 'DISCONNECT',
+ cleanup: 'CLEANUP'
+};
+
+export const urls = {
+ DRIVER_DOWNLOAD: 'http://tidepool.org/downloads/',
+ HOW_TO_UPDATE_KB_ARTICLE: 'https://tidepool-project.helpscoutdocs.com/article/6-how-to-install-or-upgrade-the-tidepool-uploader-gen'
+};
diff --git a/lib/redux/reducers/devices.js b/lib/redux/reducers/devices.js
new file mode 100644
index 0000000000..395f0eec71
--- /dev/null
+++ b/lib/redux/reducers/devices.js
@@ -0,0 +1,95 @@
+const devices = {
+ carelink: {
+ instructions: ['Import from CareLink', '(We will not store your credentials)'],
+ isFetching: false,
+ key: 'carelink',
+ name: 'Medtronic',
+ // for the device selection list
+ selectName: 'Medtronic (from CareLink)',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'carelink'},
+ enabled: {mac: true, win: true}
+ },
+ omnipod: {
+ instructions: 'Choose .ibf file from PDM',
+ key: 'omnipod',
+ name: 'Insulet OmniPod',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'block', driverId: 'InsuletOmniPod', extension: '.ibf'},
+ enabled: {mac: true, win: true}
+ },
+ dexcom: {
+ instructions: 'Plug in receiver with micro-USB',
+ key: 'dexcom',
+ name: 'Dexcom',
+ showDriverLink: {mac: true, win: true},
+ source: {type: 'device', driverId: 'Dexcom'},
+ enabled: {mac: true, win: true}
+ },
+ precisionxtra: {
+ instructions: 'Plug in meter with cable',
+ key: 'precisionxtra',
+ name: 'Abbott Precision Xtra',
+ showDriverLink: {mac: false, win: true},
+ source: {type: 'device', driverId: 'AbbottPrecisionXtra'},
+ enabled: {mac: false, win: true}
+ },
+ tandem: {
+ instructions: 'Plug in pump with micro-USB',
+ key: 'tandem',
+ name: 'Tandem',
+ showDriverLink: {mac: false, win: true},
+ source: {type: 'device', driverId: 'Tandem'},
+ enabled: {mac: true, win: true}
+ },
+ abbottfreestylelite: {
+ instructions: 'Plug in meter with cable',
+ key: 'abbottfreestylelite',
+ name: 'Abbott FreeStyle Lite',
+ showDriverLink: {mac: false, win: true},
+ source: {type: 'device', driverId: 'AbbottFreeStyleLite'},
+ enabled: {mac: false, win: true}
+ },
+ abbottfreestylefreedomlite: {
+ instructions: 'Plug in meter with cable',
+ key: 'abbottfreestylefreedomlite',
+ name: 'Abbott FreeStyle Freedom Lite',
+ showDriverLink: {mac: false, win: true},
+ source: {type: 'device', driverId: 'AbbottFreeStyleFreedomLite'},
+ enabled: {mac: false, win: true}
+ },
+ bayercontournext: {
+ instructions: 'Plug in meter with micro-USB',
+ key: 'bayercontournext',
+ name: 'Bayer Contour Next',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'device', driverId: 'BayerContourNext'},
+ enabled: {mac: true, win: true}
+ },
+ bayercontournextusb: {
+ instructions: 'Plug meter into USB port',
+ key: 'bayercontournextusb',
+ name: 'Bayer Contour Next USB',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'device', driverId: 'BayerContourNextUsb'},
+ enabled: {mac: true, win: true}
+ },
+ bayercontourusb: {
+ instructions: 'Plug meter into USB port',
+ key: 'bayercontourusb',
+ name: 'Bayer Contour USB',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'device', driverId: 'BayerContourUsb'},
+ enabled: {mac: true, win: true}
+ },
+ bayercontournextlink: {
+ instructions: 'Plug meter into USB port',
+ key: 'bayercontournextlink',
+ name: 'Bayer Contour Next Link',
+ showDriverLink: {mac: false, win: false},
+ source: {type: 'device', driverId: 'BayerContourNextLink'},
+ enabled: {mac: true, win: true}
+ }
+};
+
+export default devices;
diff --git a/lib/redux/reducers/index.js b/lib/redux/reducers/index.js
new file mode 100644
index 0000000000..d0b5060cd4
--- /dev/null
+++ b/lib/redux/reducers/index.js
@@ -0,0 +1,25 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import { combineReducers } from 'redux';
+import * as misc from './misc';
+import * as uploads from './uploads';
+import * as users from './users';
+
+const uploader = combineReducers(Object.assign(misc, uploads, users));
+
+export default uploader;
\ No newline at end of file
diff --git a/lib/redux/reducers/misc.js b/lib/redux/reducers/misc.js
new file mode 100644
index 0000000000..84e01654af
--- /dev/null
+++ b/lib/redux/reducers/misc.js
@@ -0,0 +1,164 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015-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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+import update from 'react-addons-update';
+import { combineReducers } from 'redux';
+
+import * as actionTypes from '../constants/actionTypes';
+import { pages, steps } from '../constants/otherConstants';
+import { UnsupportedError } from '../utils/errors';
+
+import initialDevices from './devices';
+
+export function devices(state = initialDevices, action) {
+ switch (action.type) {
+ case actionTypes.HIDE_UNAVAILABLE_DEVICES:
+ function filterOutUnavailable(os) {
+ let filteredDevices = {};
+ _.each(state, (device) => {
+ if (device.enabled[os] === true) {
+ filteredDevices[device.key] = device;
+ }
+ });
+ return filteredDevices;
+ }
+ return filterOutUnavailable(action.payload.os);
+ default:
+ return state;
+ }
+}
+
+export function dropdown(state = false, action) {
+ switch (action.type) {
+ case actionTypes.TOGGLE_DROPDOWN:
+ return action.payload.isVisible;
+ case actionTypes.LOGOUT_REQUEST:
+ return false;
+ default:
+ return state;
+ }
+}
+
+export function os(state = null, action) {
+ switch (action.type) {
+ case actionTypes.SET_OS:
+ return action.payload.os;
+ default:
+ return state;
+ }
+}
+
+export function page(state = pages.LOADING, action) {
+ switch (action.type) {
+ case actionTypes.SET_PAGE:
+ return action.payload.page;
+ default:
+ return state;
+ }
+}
+
+export function unsupported(state = true, action) {
+ switch (action.type) {
+ case actionTypes.INIT_APP_FAILURE:
+ case actionTypes.VERSION_CHECK_FAILURE:
+ const err = action.payload;
+ if (err instanceof UnsupportedError) {
+ return true;
+ }
+ else {
+ return err;
+ }
+ case actionTypes.VERSION_CHECK_SUCCESS:
+ return false;
+ default:
+ return state;
+ }
+}
+
+export function blipUrls(state = {}, action) {
+ switch (action.type) {
+ case actionTypes. SET_BLIP_VIEW_DATA_URL:
+ return Object.assign({}, state, {
+ viewDataLink: action.payload.url
+ });
+ case actionTypes.SET_FORGOT_PASSWORD_URL:
+ return Object.assign({}, state, {
+ forgotPassword: action.payload.url
+ });
+ case actionTypes.SET_SIGNUP_URL:
+ return Object.assign({}, state, {
+ signUp: action.payload.url
+ });
+ default:
+ return state;
+ }
+}
+
+function checkingVersion(state = false, action) {
+ switch (action.type) {
+ case actionTypes.VERSION_CHECK_FAILURE:
+ case actionTypes.VERSION_CHECK_SUCCESS:
+ return false;
+ case actionTypes.VERSION_CHECK_REQUEST:
+ return true;
+ default:
+ return state;
+ }
+}
+
+function fetchingUserInfo(state = false, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_FAILURE:
+ case actionTypes.LOGIN_SUCCESS:
+ return false;
+ case actionTypes.LOGIN_REQUEST:
+ return true;
+ default:
+ return state;
+ }
+}
+
+function initializingApp(state = true, action) {
+ switch (action.type) {
+ case actionTypes.INIT_APP_FAILURE:
+ case actionTypes.INIT_APP_SUCCESS:
+ return false;
+ case actionTypes.INIT_APP_REQUEST:
+ return true;
+ default:
+ return state;
+ }
+}
+
+function uploading(state = false, action) {
+ switch (action.type) {
+ case actionTypes.UPLOAD_REQUEST:
+ return true;
+ case actionTypes.READ_FILE_ABORTED:
+ case actionTypes.READ_FILE_FAILURE:
+ case actionTypes.UPLOAD_FAILURE:
+ case actionTypes.UPLOAD_SUCCESS:
+ return false;
+ default:
+ return state;
+ }
+}
+
+export const working = combineReducers({
+ checkingVersion, fetchingUserInfo, initializingApp, uploading
+});
diff --git a/lib/redux/reducers/uploads.js b/lib/redux/reducers/uploads.js
new file mode 100644
index 0000000000..871f78eff5
--- /dev/null
+++ b/lib/redux/reducers/uploads.js
@@ -0,0 +1,386 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+import update from 'react-addons-update';
+
+import * as actionTypes from '../constants/actionTypes';
+import { steps } from '../constants/otherConstants';
+
+export function uploadProgress(state = null, action) {
+ switch (action.type) {
+ case actionTypes.CARELINK_FETCH_REQUEST:
+ return {
+ percentage: 0,
+ step: steps.carelinkFetch
+ };
+ case actionTypes.DEVICE_DETECT_REQUEST:
+ return {
+ percentage: 0,
+ step: steps.detect
+ };
+ case actionTypes.UPLOAD_FAILURE:
+ case actionTypes.UPLOAD_SUCCESS:
+ return null;
+ case actionTypes.UPLOAD_PROGRESS:
+ return Object.assign({}, state, action.payload);
+ case actionTypes.UPLOAD_REQUEST:
+ return {
+ percentage: 0,
+ step: steps.start
+ };
+ default:
+ return state;
+ }
+}
+
+export function uploadsByUser(state = {}, action) {
+ switch (action.type) {
+ case actionTypes.CARELINK_FETCH_FAILURE:
+ let uploadTargetUser;
+ const uploadTargetDevice = 'carelink';
+ _.forOwn(state, (uploads, userId) => {
+ _.forOwn(uploads, (upload, deviceKey) => {
+ if (deviceKey === 'carelink' && upload.isFetching === true) {
+ uploadTargetUser = userId;
+ }
+ });
+ });
+ if (uploadTargetUser) {
+ return update(
+ state,
+ {[uploadTargetUser]: {[uploadTargetDevice]: {
+ isFetching: {$set: false}
+ }}}
+ );
+ }
+ case actionTypes.CARELINK_FETCH_REQUEST: {
+ const { userId, deviceKey } = action.payload;
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {isFetching: {$set: true}}}}
+ );
+ }
+ case actionTypes.CARELINK_FETCH_SUCCESS: {
+ const { userId, deviceKey } = action.payload;
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {isFetching: {$set: false}}}}
+ );
+ }
+ case actionTypes.CHOOSING_FILE: {
+ const { userId, deviceKey } = action.payload;
+ let newState = state;
+ let devicesForCurrentUser = _.get(state, [userId], {});
+ _.forOwn(devicesForCurrentUser, (upload, key) => {
+ newState = update(
+ newState,
+ {[userId]: {[key]: {$apply: (upload) => {
+ if (key === deviceKey) {
+ return update(
+ upload,
+ {
+ choosingFile: {$set: true}
+ }
+ );
+ }
+ else {
+ return update(
+ upload,
+ {disabled: {$set: true}}
+ );
+ }
+ }}}}
+ );
+ });
+ return newState;
+ }
+ case actionTypes.READ_FILE_ABORTED: {
+ const err = action.payload;
+ let uploadTargetUser, uploadTargetDevice;
+ _.forOwn(state, (uploads, userId) => {
+ _.forOwn(uploads, (upload, deviceKey) => {
+ if (upload.choosingFile === true) {
+ uploadTargetUser = userId;
+ uploadTargetDevice = deviceKey;
+ }
+ });
+ });
+ if (uploadTargetUser && uploadTargetDevice) {
+ return update(
+ state,
+ {[uploadTargetUser]: {[uploadTargetDevice]: {
+ choosingFile: {$set: false},
+ completed: {$set: true},
+ error: {$set: err},
+ failed: {$set: true}
+ }}}
+ );
+ }
+ }
+ case actionTypes.READ_FILE_FAILURE: {
+ const err = action.payload;
+ let uploadTargetUser, uploadTargetDevice;
+ _.forOwn(state, (uploads, userId) => {
+ _.forOwn(uploads, (upload, deviceKey) => {
+ if (upload.readingFile === true) {
+ uploadTargetUser = userId;
+ uploadTargetDevice = deviceKey;
+ }
+ });
+ });
+ if (uploadTargetUser && uploadTargetDevice) {
+ return update(
+ state,
+ {[uploadTargetUser]: {[uploadTargetDevice]: {
+ completed: {$set: true},
+ error: {$set: err},
+ failed: {$set: true},
+ readingFile: {$set: false}
+ }}}
+ );
+ }
+ }
+ case actionTypes.READ_FILE_REQUEST: {
+ const { userId, deviceKey, filename } = action.payload;
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {
+ choosingFile: {$set: false},
+ file: {$set: {name: filename}},
+ readingFile: {$set: true}
+ }}}
+ );
+ }
+ case actionTypes.READ_FILE_SUCCESS: {
+ const { userId, deviceKey, filedata } = action.payload;
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {
+ file: {data: {$set: filedata}},
+ readingFile: {$set: false}
+ }}}
+ );
+ }
+ case actionTypes.RESET_UPLOAD: {
+ const { userId, deviceKey } = action.payload;
+ const uploadInProgress = _.some(
+ _.get(state, [userId], {}),
+ (upload, key) => {
+ const fileDataExists = _.get(upload, ['file', 'data'], null) !== null;
+ // because we don't want the existence of file.data on a block-mode device
+ // to make it appear as though an upload is in progress when we're trying
+ // to reset the block-mode device itself!
+ if (key !== deviceKey) {
+ return upload.choosingFile ||
+ upload.readingFile ||
+ (fileDataExists && !upload.completed) ||
+ upload.uploading;
+ }
+ else {
+ return false;
+ }
+ }
+ );
+ if (uploadInProgress) {
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {$apply: (upload) => {
+ let resetUpload = _.pick(upload, 'history');
+ resetUpload.disabled = true;
+ return resetUpload;
+ }}}}
+ );
+ }
+ else {
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {$apply: (upload) => {
+ return _.pick(upload, 'history');
+ }}}}
+ );
+ }
+ }
+ case actionTypes.SET_UPLOADS:
+ const { devicesByUser } = action.payload;
+ let newState = state;
+ _.forOwn(devicesByUser, (deviceKeys, userId) => {
+ if (_.get(newState, userId, null) === null) {
+ const uploadsForUser = {};
+ _.each(deviceKeys, (deviceKey) => {
+ uploadsForUser[deviceKey] = {history: []};
+ });
+ newState = update(
+ newState,
+ {[userId]: {$set: uploadsForUser}}
+ );
+ }
+ else {
+ _.each(deviceKeys, (deviceKey) => {
+ if (_.get(newState, [userId, deviceKey], null) === null) {
+ newState = update(
+ newState,
+ {[userId]: {[deviceKey]: {$set: {history: []}}}}
+ );
+ }
+ });
+ const devicesToDelete = _.difference(
+ Object.keys(newState[userId]),
+ deviceKeys
+ );
+ if (!_.isEmpty(devicesToDelete)) {
+ newState = update(
+ newState,
+ {[userId]: {$apply: (uploadsForUser) => {
+ _.each(devicesToDelete, (deviceKey) => {
+ uploadsForUser = _.omit(uploadsForUser, deviceKey);
+ });
+ return uploadsForUser;
+ }}}
+ );
+ }
+ }
+ });
+ return newState;
+ case actionTypes.TOGGLE_ERROR_DETAILS: {
+ const { userId, deviceKey, isVisible } = action.payload;
+ return update(
+ state,
+ {[userId]: {[deviceKey]: {showErrorDetails: {$set: isVisible}}}}
+ );
+ }
+ case actionTypes.UPLOAD_FAILURE: {
+ const err = action.payload;
+ let uploadTargetUser, uploadTargetDevice;
+ _.forOwn(state, (uploads, userId) => {
+ _.forOwn(uploads, (upload, deviceKey) => {
+ if (upload.uploading === true) {
+ uploadTargetUser = userId;
+ uploadTargetDevice = deviceKey;
+ }
+ });
+ });
+ if (uploadTargetUser && uploadTargetDevice) {
+ let newState = state;
+ let devicesForCurrentUser = _.get(state, [uploadTargetUser], {});
+ _.forOwn(devicesForCurrentUser, (upload, key) => {
+ newState = update(
+ newState,
+ {[uploadTargetUser]: {[key]: {$apply: (upload) => {
+ if (key === uploadTargetDevice) {
+ return update(
+ upload,
+ {
+ completed: {$set: true},
+ error: {$set: err},
+ failed: {$set: true},
+ history: {[0]: {
+ error: {$set: true},
+ finish: {$set: err.utc}
+ }},
+ uploading: {$set: false}
+ }
+ );
+ }
+ else {
+ return _.omit(upload, 'disabled');
+ }
+ }}}}
+ );
+ });
+ return newState;
+ }
+ }
+ case actionTypes.UPLOAD_REQUEST: {
+ const { userId, deviceKey, utc } = action.payload;
+ let newState = state;
+ let devicesForCurrentUser = _.get(state, [userId], {});
+ _.forOwn(devicesForCurrentUser, (upload, key) => {
+ newState = update(
+ newState,
+ {[userId]: {[key]: {$apply: (upload) => {
+ if (key === deviceKey) {
+ return update(
+ upload,
+ {
+ history: {$unshift: [{start: utc}]},
+ uploading: {$set: true}
+ }
+ );
+ }
+ else {
+ return update(
+ upload,
+ {disabled: {$set: true}}
+ );
+ }
+ }}}}
+ );
+ });
+ return newState;
+ }
+ case actionTypes.UPLOAD_SUCCESS: {
+ const { userId, deviceKey, data, utc } = action.payload;
+ let newState = state;
+ let devicesForCurrentUser = _.get(state, [userId], {});
+ _.forOwn(devicesForCurrentUser, (upload, key) => {
+ newState = update(
+ newState,
+ {[userId]: {[key]: {$apply: (upload) => {
+ if (key === deviceKey) {
+ return update(
+ upload,
+ {
+ completed: {$set: true},
+ data: {$set: data},
+ history: {[0]: {
+ finish: {$set: utc}
+ }},
+ successful: {$set: true},
+ uploading: {$set: false}
+ }
+ );
+ }
+ else {
+ return _.omit(upload, 'disabled');
+ }
+ }}}}
+ );
+ });
+ return newState;
+ }
+ default:
+ return state;
+ }
+}
+
+export function uploadTargetDevice(state = null, action) {
+ switch (action.type) {
+ case actionTypes.CHOOSING_FILE:
+ case actionTypes.UPLOAD_REQUEST: {
+ const { deviceKey } = action.payload;
+ return deviceKey;
+ }
+ case actionTypes.READ_FILE_ABORTED:
+ case actionTypes.READ_FILE_FAILURE:
+ case actionTypes.UPLOAD_FAILURE:
+ case actionTypes.UPLOAD_SUCCESS:
+ return null;
+ default:
+ return state;
+ }
+}
diff --git a/lib/redux/reducers/users.js b/lib/redux/reducers/users.js
new file mode 100644
index 0000000000..c13051ecf0
--- /dev/null
+++ b/lib/redux/reducers/users.js
@@ -0,0 +1,242 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+import update from 'react-addons-update';
+
+import * as actionTypes from '../constants/actionTypes';
+
+export function allUsers(state = {}, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN:
+ const { user, profile, memberships } = action.payload;
+ let newState = {};
+ _.each(memberships, (membership) => {
+ newState[membership.userid] = (membership.userid === user.userid) ?
+ Object.assign({}, _.omit(user, 'userid'), profile) :
+ Object.assign({}, membership.profile);
+ });
+ return newState;
+ case actionTypes.LOGOUT_REQUEST:
+ return {};
+ default:
+ return state;
+ }
+}
+
+export function loggedInUser(state = null, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN:
+ const { user } = action.payload;
+ return user.userid;
+ case actionTypes.LOGOUT_REQUEST:
+ return null;
+ default:
+ return state;
+ }
+}
+
+export function loginErrorMessage(state = null, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_FAILURE:
+ const err = action.payload;
+ return err.message;
+ case actionTypes.LOGIN_REQUEST:
+ return null;
+ default:
+ return state;
+ }
+}
+
+function isPwd(membership) {
+ return !_.isEmpty(_.get(membership, ['profile', 'patient'], {}));
+}
+
+export function targetDevices(state = {}, action) {
+ switch (action.type) {
+ case actionTypes.ADD_TARGET_DEVICE: {
+ const { userId, deviceKey } = action.payload;
+ return update(
+ state,
+ {[userId]: {$apply: (devicesArray) => {
+ if (devicesArray == null) {
+ return [deviceKey];
+ }
+ else if (!_.includes(devicesArray, deviceKey)) {
+ let newDevices = devicesArray.slice(0);
+ newDevices.push(deviceKey);
+ return newDevices;
+ }
+ else {
+ return devicesArray;
+ }
+ }}}
+ );
+ }
+ // create some scaffolding based on the users the loggedInUser
+ // currently has upload access to
+ // (since what's in localStorage could be outdated)
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN: {
+ const { memberships } = action.payload;
+ let newState = {};
+ _.each(memberships, (membership) => {
+ if (isPwd(membership)) {
+ newState[membership.userid] = [];
+ }
+ });
+ return newState;
+ }
+ case actionTypes.LOGOUT_REQUEST:
+ return {};
+ case actionTypes.REMOVE_TARGET_DEVICE: {
+ const { userId, deviceKey } = action.payload;
+ return update(
+ state,
+ {[userId]: {$apply: (devices) => {
+ return _.filter(devices, (device) => {
+ return device !== deviceKey;
+ });
+ }}}
+ );
+ }
+ case actionTypes.SET_USERS_TARGETS: {
+ const { targets } = action.payload;
+ let newState = state;
+ _.forOwn(targets, (targetsArray, userId) => {
+ if (newState[userId] != null) {
+ const targetDevices = _.pluck(targetsArray, 'key');
+ newState = update(
+ newState,
+ {[userId]: {$set: targetDevices}}
+ );
+ }
+ });
+ return newState;
+ }
+ case actionTypes.STORING_USERS_TARGETS:
+ // _.omit returns a new object, doesn't mutate
+ return _.omit(state, 'noUserSelected');
+ default:
+ return state;
+ }
+}
+
+export function targetTimezones(state = {}, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN:
+ const { memberships } = action.payload;
+ let newState = {};
+ _.each(memberships, (membership) => {
+ if (isPwd(membership)) {
+ newState[membership.userid] = null;
+ }
+ });
+ return newState;
+ case actionTypes.LOGOUT_REQUEST:
+ return {};
+ case actionTypes.SET_TARGET_TIMEZONE: {
+ const { userId, timezoneName } = action.payload;
+ return update(
+ state,
+ {[userId]: {$set: timezoneName}}
+ );
+ }
+ case actionTypes.SET_USERS_TARGETS: {
+ const { targets } = action.payload;
+ let newState = state;
+ _.forOwn(targets, (targetsArray, userId) => {
+ // we have to check *specifically* for undefined here
+ // because we use null when there isn't a timezone
+ if (newState[userId] !== undefined) {
+ const targetTimezones = _.uniq(_.pluck(targetsArray, 'timezone'));
+ if (targetTimezones.length === 1) {
+ newState = update(
+ newState,
+ {[userId]: {$set: targetTimezones[0]}}
+ );
+ }
+ // if different timezones are stored for different devices
+ // we set to `null` to force the user to choose again
+ else {
+ newState = update(
+ newState,
+ {[userId]: {$set: null}}
+ );
+ }
+ }
+ });
+ return newState;
+ }
+ case actionTypes.STORING_USERS_TARGETS:
+ // _.omit returns a new object, doesn't mutate
+ return _.omit(state, 'noUserSelected');
+ default:
+ return state;
+ }
+}
+
+export function targetUsersForUpload(state = [], action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN:
+ const { user, profile, memberships } = action.payload;
+ let newState = [];
+ _.each(memberships, (membership) => {
+ if (membership.userid === user.userid) {
+ if (!_.isEmpty(profile.patient)) {
+ newState.push(membership.userid);
+ }
+ }
+ else {
+ newState.push(membership.userid);
+ }
+ });
+ return newState;
+ case actionTypes.LOGOUT_REQUEST:
+ return [];
+ default:
+ return state;
+ }
+}
+
+export function uploadTargetUser(state = null, action) {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ case actionTypes.SET_USER_INFO_FROM_TOKEN:
+ const { user, profile, memberships } = action.payload;
+ if (!_.isEmpty(profile.patient)) {
+ return user.userid;
+ }
+ else if (memberships.length === 1) {
+ return memberships[0].userid;
+ }
+ else {
+ return null;
+ }
+ case actionTypes.SET_UPLOAD_TARGET_USER:
+ const { userId } = action.payload;
+ return userId;
+ case actionTypes.LOGOUT_REQUEST:
+ return null;
+ default:
+ return state;
+ }
+}
diff --git a/lib/redux/store/configureStore.js b/lib/redux/store/configureStore.js
new file mode 100644
index 0000000000..e02791de82
--- /dev/null
+++ b/lib/redux/store/configureStore.js
@@ -0,0 +1,60 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/* global __REDUX_DEV_UI__, __REDUX_LOG__ */
+
+import { compose, createStore } from 'redux';
+
+import api from '../../core/api';
+import config from '../../config';
+api.create({
+ apiUrl: config.API_URL,
+ uploadUrl: config.UPLOAD_URL,
+ version: config.version
+});
+
+// without this, right-click env-changing menu won't work!
+window.api = api;
+
+import uploader from '../reducers/';
+
+import DevTools from '../../components/DevTools';
+
+let middlewares;
+
+if (__REDUX_LOG__ === true) {
+ middlewares = require('./middlewares.dev.js')(api);
+} else {
+ middlewares = require('./middlewares.prod.js')(api);
+}
+
+let finalCreateStore;
+
+if (__REDUX_DEV_UI__ === true) {
+ finalCreateStore = compose(
+ middlewares,
+ DevTools.instrument()
+ )(createStore);
+}
+else {
+ finalCreateStore = compose(middlewares)(createStore);
+}
+
+export default function configureStore(initialState) {
+ const store = finalCreateStore(uploader, initialState);
+ return { api, store, version: config.version};
+}
diff --git a/lib/redux/store/middlewares.dev.js b/lib/redux/store/middlewares.dev.js
new file mode 100644
index 0000000000..968d705153
--- /dev/null
+++ b/lib/redux/store/middlewares.dev.js
@@ -0,0 +1,37 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+
+import { applyMiddleware } from 'redux';
+import createLogger from 'redux-logger';
+import thunk from 'redux-thunk';
+
+import { createErrorLogger } from '../utils/errors';
+import { createMetricsTracker } from '../utils/metrics';
+
+export default (api) => applyMiddleware(
+ /*
+ * order is significant here!
+ * in particular, the thunk middleware must be applied first
+ * redux middleware doc is a work of easily-understood genius:
+ * http://redux.js.org/docs/advanced/Middleware.html
+ */
+ thunk,
+ createLogger(),
+ createErrorLogger(api),
+ createMetricsTracker(api)
+);
diff --git a/lib/redux/store/middlewares.prod.js b/lib/redux/store/middlewares.prod.js
new file mode 100644
index 0000000000..e5af18be93
--- /dev/null
+++ b/lib/redux/store/middlewares.prod.js
@@ -0,0 +1,35 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+
+import { applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+
+import { createErrorLogger } from '../utils/errors';
+import { createMetricsTracker } from '../utils/metrics';
+
+export default (api) => applyMiddleware(
+ /*
+ * order is significant here!
+ * in particular, the thunk middleware must be applied first
+ * redux middleware doc is a work of easily-understood genius:
+ * http://redux.js.org/docs/advanced/Middleware.html
+ */
+ thunk,
+ createErrorLogger(api),
+ createMetricsTracker(api)
+);
diff --git a/lib/redux/utils/errors.js b/lib/redux/utils/errors.js
new file mode 100644
index 0000000000..6e5c3225e1
--- /dev/null
+++ b/lib/redux/utils/errors.js
@@ -0,0 +1,106 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015-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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+
+export const errorText = {
+ E_CARELINK_CREDS: 'Check your CareLink username and password',
+ E_CARELINK_UPLOAD: 'Error processing & uploading CareLink data',
+ E_DEVICE_UPLOAD: 'Something went wrong during device upload',
+ E_FETCH_CARELINK: 'Something went wrong trying to fetch CareLink data',
+ E_FILE_EXT: 'Please choose a file ending in ',
+ E_HID_CONNECTION: 'Hmm, your device doesn\'t appear to be connected',
+ E_INIT: 'Error during app initialization',
+ E_OFFLINE: 'Not connected to the Internet!',
+ E_READ_FILE: 'Error reading file ',
+ E_SERIAL_CONNECTION: 'Hmm, we couldn\'t detect your device',
+ E_SERVER_ERR: 'Sorry, the Tidepool servers appear to be down',
+ E_UPLOAD_IN_PROGRESS: 'Sorry, an upload is already in progress'
+};
+
+const errorProps = {
+ code: 'Code',
+ details: 'Details',
+ name: 'Name',
+ step: 'Driver Step',
+ stringifiedStack: 'Stack Trace',
+ utc: 'UTC Time',
+ version: 'Version'
+};
+
+export function addInfoToError(err, props) {
+ let debug = [];
+ _.forOwn(props, (v, k) => {
+ if (!_.isEmpty(v) && v !== err.message) {
+ err[k] = v;
+ debug.push(`${errorProps[k]}: ${v}`);
+ }
+ });
+ if (!_.isEmpty(debug)) {
+ err.debug = debug.join(' | ');
+ }
+ return err;
+}
+
+export function getAppInitErrorMessage(status) {
+ switch(status) {
+ case 503:
+ return errorText.E_OFFLINE;
+ default:
+ return errorText.E_INIT;
+ }
+}
+
+export function getLoginErrorMessage(status) {
+ switch(status) {
+ case 400:
+ return 'We need your e-mail to log you in!';
+ case 401:
+ return 'Please check your e-mail and password.';
+ default:
+ return 'We couldn\'t log you in. Try again in a few minutes.';
+ }
+}
+
+export function getLogoutErrorMessage() {
+ return 'Sorry, error attempting to log out.';
+}
+
+export function createErrorLogger(api) {
+ return ({ getState }) => (next) => (action) => {
+ if (_.get(action, 'error', false) === true) {
+ let err = _.get(action, 'payload', {});
+ if (!err.debug) {
+ err.debug = err.message || 'Unknown error';
+ }
+ api.errors.log(
+ err,
+ _.get(action, 'meta.metric.eventName', null),
+ _.omit(_.get(action, 'meta.metric.properties', {}), 'error')
+ );
+ }
+ return next(action);
+ };
+}
+
+export function UnsupportedError(currentVersion, requiredVersion) {
+ this.name = 'UnsupportedError';
+ this.message = `Uploader version ${currentVersion} is no longer supported; version ${requiredVersion} or higher is required.`;
+}
+
+UnsupportedError.prototype = Object.create(Error.prototype);
+UnsupportedError.prototype.constructor = UnsupportedError;
diff --git a/lib/redux/utils/metrics.js b/lib/redux/utils/metrics.js
new file mode 100644
index 0000000000..aab83f64a8
--- /dev/null
+++ b/lib/redux/utils/metrics.js
@@ -0,0 +1,32 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+import _ from 'lodash';
+
+const NONE_PROVIDED = 'No Event Name Provided';
+
+export function createMetricsTracker(api) {
+ return ({ getState }) => (next) => (action) => {
+ if (_.get(action, 'meta.metric', null) !== null) {
+ api.metrics.track(
+ _.get(action, 'meta.metric.eventName', NONE_PROVIDED),
+ _.get(action, 'meta.metric.properties', {})
+ );
+ }
+ return next(action);
+ };
+}
diff --git a/lib/serialDevice.js b/lib/serialDevice.js
index b74bc0d466..41e572be5c 100644
--- a/lib/serialDevice.js
+++ b/lib/serialDevice.js
@@ -266,11 +266,14 @@ module.exports = function(config) {
// TODO: Dexcom-specific code to be removed,
// once OSX El Capitan is updated to not require Dexcom restart
- if((this.app.state._os === 'mac') && (deviceInfo.driverId === 'Dexcom') && (deviceInfo.path.match(deviceInfo.portPattern) === null)) {
- return cb(new Error('You may also need to power off and on your Dexcom receiver. ' + deviceDebugInfo));
- }
+ chrome.runtime.getPlatformInfo(function(platformInfo) {
+ if ((platformInfo.os === 'mac') && (deviceInfo.driverId === 'Dexcom') &&
+ (deviceInfo.path.match(deviceInfo.portPattern) === null)) {
+ return cb(new Error('You may also need to power off and on your Dexcom receiver. ' + deviceDebugInfo));
+ }
- cb(new Error('Could not connect to a matching device port: ' + deviceDebugInfo));
+ cb(new Error('Could not connect to a matching device port: ' + deviceDebugInfo));
+ });
}
});
});
diff --git a/lib/state/appActions.js b/lib/state/appActions.js
deleted file mode 100644
index 1cc9c1e9c2..0000000000
--- a/lib/state/appActions.js
+++ /dev/null
@@ -1,853 +0,0 @@
-/*
- * == BSD2 LICENSE ==
- * Copyright (c) 2014, 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
- * License as published by the Open Source Initiative at opensource.org.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the License for more details.
- *
- * You should have received a copy of the License along with this program; if
- * not, you can obtain one from Tidepool Project at tidepool.org.
- * == BSD2 LICENSE ==
- */
- /* global chrome */
-
-var _ = require('lodash');
-var async = require('async');
-var stacktrace = require('stack-trace');
-
-var sundial = require('sundial');
-var localStore = require('../core/localStore');
-var api = require('../core/api');
-var device = require('../core/device');
-var carelink = require('../core/carelink');
-var appState = require('./appState');
-
-var config = require('../config.js');
-
-var appActions = {};
-
-appActions.trackedState = {
- LOGIN_SUCCESS : 'Login Successful',
- LOGOUT_CLICKED : 'Clicked Log Out',
- UPLOAD_FAILED : 'Upload Failed',
- UPLOAD_SUCCESS : 'Upload Successful',
- UPLOAD_STARTED : 'Upload Attempted',
- CARELINK_FETCH_FAILED : 'CareLink Fetch Failed',
- CARELINK_FETCH_SUCCESS : 'CareLink Fetch Successful',
- CARELINK_LOAD_FAILED : 'CareLink Upload Failed',
- CARELINK_LOAD_SUCCESS : 'CareLink Upload Successful',
- SEE_IN_BLIP : 'Clicked See Data in Blip'
-};
-
-appActions.errorText = {
- E_READING_FILE : 'Error reading file: ',
- E_WRONG_FILE_EXT : 'Please choose a file ending in ',
- E_UPLOAD_IN_PROGRESS : 'Cannot start upload while an upload is in progress',
- E_UNSUPPORTED_TYPE : 'Unsupported upload source type: ',
- E_INVALID_UPLOAD_INDEX : 'Invalid upload index: ',
- E_DEVICE_NOT_CONNECTED : 'The device doesn\'t appear to be connected'
-};
-
-appActions.errorStages = {
- STAGE_SETUP : { code: 'E_SETUP' , friendlyMessage: 'Error during setup' },
- STAGE_LOGIN : { code: 'E_LOGIN' , friendlyMessage: 'Error during login' },
- STAGE_AFTER_LOGIN : { code: 'E_AFTER_LOGIN' , friendlyMessage: 'You have no upload access - please set up data storage in blip or ask for permission to upload to another person\'s account.' },
- STAGE_LOGOUT : { code: 'E_LOGOUT' , friendlyMessage: 'Error during logout' },
- STAGE_DETECT_DEVICES : { code: 'E_DETECTING_DEVICES' , friendlyMessage: 'Error detecting devices' },
- STAGE_PICK_FILE : { code: 'E_SELECTING_FILE' , friendlyMessage: 'Error during file selection' },
- STAGE_READ_FILE : { code: 'E_READING_FILE' , friendlyMessage: 'Error trying to read file' },
- STAGE_UPLOAD : { code: 'E_UPLOADING' , friendlyMessage: 'Error uploading data' },
- STAGE_METADATA_UPLOAD : { code: 'E_METADATA_UPLOAD' , friendlyMessage: 'Error uploading upload metadata'},
- STAGE_DEVICE_UPLOAD : { code: 'E_DEVICE_UPLOAD' , friendlyMessage: 'Error uploading device data' },
- STAGE_DEVICE_DETECT : { code: 'E_DEVICE_DETECT' , friendlyMessage: 'Hmm, we couldn\'t detect your device' },
- STAGE_DRIVER : { code: 'E_DRIVER' , friendlyMessage: 'You may need to install the ', driverName: 'device driver' },
- STAGE_CARELINK_UPLOAD : { code: 'E_CARELINK_UPLOAD' , friendlyMessage: 'Error uploading CareLink data' },
- STAGE_CARELINK_FETCH : { code: 'E_CARELINK_FETCH' , friendlyMessage: 'Error fetching CareLink data' }
-};
-
-function extractMessage(err) {
- // if there's no message, return `error` or 'Unknown error message'
- if (_.isEmpty(err.message)) {
- return err.error || 'Unknown error message';
- }
- return err.message;
-}
-
-function getErrorName(err) {
- if (err.name) {
- return err.name;
- }
- return 'POST error';
-}
-
-function getTargetTimezone(deviceList) {
- var uniqTimezones = _.uniq(_.pluck(deviceList, 'timezone'));
- if (uniqTimezones.length === 1) {
- return uniqTimezones[0];
- }
- // if we find more than one timezone or 0 timezones, we return null
- // and you'll get sent back to the settings page (or stay there)
- else {
- return null;
- }
-}
-
-appActions.addMoreInfoToError = function(err, stage) {
- var name = err.name;
- err.version = config.namedVersion;
- // the current hack for CareLink incorrect creds throws an error with the code
- // and friendlyMessage already set, so we don't want to steamroll it
- err.code = err.code || stage.code;
- err.friendlyMessage = err.friendlyMessage || stage.friendlyMessage;
- err.driverLink = stage.driverLink;
- err.driverName = stage.driverName;
- err.stringifiedStack = _.pluck(_.filter(stacktrace.parse(err), function(cs) { return cs.functionName !== null; }), 'functionName').join(', ');
- err.debug = 'Detail: ' + extractMessage(err) + ' | ' + 'Error UTC Time: ' +
- sundial.utcDateString() + ' | ' + 'Code: ' + err.code + ' | ' + 'Error Type: ' +
- getErrorName(err) + ' | ' + 'Version: ' + err.version;
- if (err.step != null) {
- err.debug = err.debug + ' | ' + 'Driver Step: ' + err.step;
- }
- if (err.stringifiedStack) {
- err.debug = err.debug + ' | ' + 'Stack Trace: ' + err.stringifiedStack;
- }
-};
-
-appActions.bindApp = function(app) {
- this.app = app;
- return this;
-};
-
-appActions.changeGroup = function(userid) {
- // reset upload state when changing group
- for(var i in this.app.state.uploads) {
- appActions.reset(i);
- }
-
- // we are storing the timezone on a per device basis
- var devicesById = localStore.getItem('devices') || {};
- var targetDevices = devicesById[userid] || [];
- var targetTimezone = getTargetTimezone(targetDevices);
-
- if (_.isEmpty(targetDevices) || _.isEmpty(targetTimezone)) {
- this.app.setState({
- targetId: userid,
- targetDevices: targetDevices,
- targetTimezone: targetTimezone,
- page: 'settings'
- });
- }
- this.app.setState({
- targetId: userid,
- targetDevices: _.pluck(targetDevices, 'key'),
- targetTimezone: targetTimezone
- });
-};
-
-appActions.changeTimezone = function(timezone) {
- this.app.setState({
- targetTimezone: timezone
- });
-};
-
-appActions.load = function(cb) {
- var self = this;
- var loadLocalStore = function(cb) {
- localStore.init(localStore.getInitialState(), function() {
- cb();
- });
- };
-
- var getOS = function(cb) {
- if (typeof chrome !== 'undefined') { // test environment is not chrome
- chrome.runtime.getPlatformInfo(function (platformInfo) {
- self.app.setState({_os: platformInfo.os}, function() {
- self._hideUnavailableDevices();
- });
- cb();
- });
- }else{
- cb();
- }
- };
-
- function setHostsWithCallback(cb) {
- api.setHosts(_.pick(config, ['API_URL', 'UPLOAD_URL', 'BLIP_URL']));
- return cb(null);
- }
-
- async.series([
- loadLocalStore,
- getOS,
- device.init.bind(device, {
- api: api,
- version: config.namedVersion
- }),
- carelink.init.bind(carelink, {
- api: api
- }),
- api.init.bind(api, {
- // these are for initialization only -- they get overwritten
- // if someone changes the URL
- apiUrl: config.API_URL,
- uploadUrl: config.UPLOAD_URL
- }),
- setHostsWithCallback,
- ], function(err, results) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_SETUP']);
- return cb(err);
- }
-
- var session = results[4];
- if (!session) {
- self.app.setState({
- page: 'login'
- });
- return cb();
- }
-
- async.series([
- api.user.account.bind(null),
- api.user.profile.bind(null),
- api.user.getUploadGroups.bind(null)
- ], function(err, results) {
- var account = results[0];
- var profile = results[1];
- var uploadGroups = results[2];
-
- var user = _.assign({}, account, {profile: profile, uploadGroups: uploadGroups});
-
- // once we've logged in, we don't want the right-click menus around, so
- // delete them.
- // because our test env isn't chrome, we have to test for it
- if (typeof chrome !== 'undefined') {
- chrome.contextMenus.removeAll();
- }
-
- appActions._afterLoginPage(user, cb);
- });
- });
-};
-
-appActions.login = function(credentials, options, cb) {
- var self = this;
- async.series([
- api.user.login.bind(null, credentials, options),
- api.user.profile,
- api.user.getUploadGroups.bind(null)
- ], function(err, results) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_LOGIN']);
- return cb(err);
- }
- self._logMetric(self.trackedState.LOGIN_SUCCESS);
- // once we've logged in, we don't want the right-click menus around, so
- // delete them.
- // because our test env isn't chrome, we have to test for it
- if (typeof chrome !== 'undefined') {
- chrome.contextMenus.removeAll();
- }
-
- var account = results[0] && results[0].user;
- var profile = results[1];
- var uploadGroups = results[2];
-
- var user = _.assign({}, account, {profile: profile, uploadGroups: uploadGroups});
-
- appActions._afterLoginPage(user, cb);
- });
-};
-
-// make sure we have the correct default targetId
-appActions._getDefaultTargetId = function(loggedInUser){
- // default to the logged in user's id
- var userid = loggedInUser.userid;
-
- var possibilities = _.filter(loggedInUser.uploadGroups, function(group) {
- // if the account isn't marked with patient details we don't upload to it
- return _.isEmpty(group.profile.patient) === false;
- });
-
- // is there only one possible default?
- if (possibilities.length === 1) {
- return possibilities[0].userid;
- }
- else if (possibilities.length === 0) {
- // TODO: this should surface as a new app 'page' with an error message
- // stating that the logged in user doesn't have data storage set up for his/herself
- // nor upload permission to any other accounts
- // then there should be a prompting to go to blip and set up data storage
- var err = new Error('No data storage for logged in user and no permissions to upload for others!');
- this.addMoreInfoToError(err, this.errorStages['STAGE_AFTER_LOGIN']);
- console.warn(err.debug);
- }
- else if (possibilities.length > 1) {
- if (_.isEmpty(loggedInUser.profile.patient)) {
- return null;
- }
- }
- return userid;
-};
-
-appActions._afterLoginPage = function(user, cb) {
- var self = this;
-
- var defaultTargetId = self._getDefaultTargetId(user);
- var devices = localStore.getItem('devices') || {};
- var targetDevicesWithTimezones = devices[defaultTargetId];
- var targetDevices = _.pluck(targetDevicesWithTimezones, 'key');
- var targetTimezone = getTargetTimezone(targetDevicesWithTimezones);
-
- if (!_.isEmpty(targetDevices) && !_.isEmpty(targetTimezone)) {
- self.app.setState({
- user: user,
- targetId: defaultTargetId,
- targetDevices: targetDevices,
- targetTimezone: targetTimezone,
- page: 'main'
- });
-
- return cb(null, user);
- }
-
- self.app.setState({
- user: user,
- targetId: defaultTargetId,
- targetDevices: [],
- targetTimezone: null,
- page: 'settings'
- });
-
- return cb(null, user);
-};
-
-appActions.logout = function(cb) {
- var self = this;
- self._logMetric(this.trackedState.LOGOUT_CLICKED);
- api.user.logout(function(err) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_LOGOUT']);
- return cb(err);
- }
-
- self.app.setState(_.assign(appState.getInitial(), {
- page: 'login'
- }));
-
- return cb();
- });
-};
-
-appActions.viewData = function() {
- this._logMetric(this.trackedState.SEE_IN_BLIP);
- return;
-};
-
-appActions.detectDevices = function(cb) {
- var self = this;
- device.detectAll(function(err, devices) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DETECT_DEVICES']);
- return cb(err);
- }
-
- var uploads = self._mergeDevicesWithUploads(
- devices, self.app.state.uploads
- );
- self.app.setState({
- uploads: uploads
- });
-
- return cb(null, devices);
- });
-};
-
-appActions._hideUnavailableDevices = function() {
- var uploads = _.cloneDeep(this.app.state.uploads);
- // this.app._os can be "mac", "win", "android", "cros", "linux", or "openbsd"
- if (this.app.state._os === 'mac') {
- uploads = _.reject(uploads, function(d) {
- return d.key === 'precisionxtra' ||
- d.key === 'abbottfreestylelite' ||
- d.key === 'abbottfreestylefreedomlite';
- });
- }
- else if (this.app.state._os === 'win') {
- uploads = _.reject(uploads, function(d) {
- return;
- });
- }
- this.app.setState({uploads: uploads});
-};
-
-appActions.chooseDevices = function() {
- this.app.setState({
- page: 'settings',
- dropMenu: false
- });
-};
-
-appActions.addOrRemoveTargetDevice = function(e) {
- var targetDevices = this.app.state.targetDevices;
- if (e.target.checked) {
- targetDevices.push(e.target.value);
- }
- else {
- targetDevices = _.reject(targetDevices, function(device) {
- return device === e.target.value;
- });
- }
-
- this.app.setState({
- targetDevices: targetDevices
- });
-};
-
-appActions.storeUserTargets = function(targetId) {
- // timezone
- var targetTimezone = this.app.state.targetTimezone;
- // devices
- var devicesById = localStore.getItem('devices') || {};
- var targetDevices = this.app.state.targetDevices;
- var devicesWithTimezoneToStore = _.map(targetDevices, function(deviceKey) {
- return {
- key: deviceKey,
- timezone: targetTimezone
- };
- });
- devicesById[targetId] = devicesWithTimezoneToStore;
- localStore.setItem('devices', devicesById);
-
- // the Done button *should* be disabled preventing this function from
- // even firing when no timezone is selected
- // so this is just an extra defensive check (which also has a unit test, natch)
- if (!_.isEmpty(targetTimezone)) {
- this.app.setState({
- page: 'main'
- });
- }
-};
-
-appActions.hideDropMenu = function() {
- this.app.setState({
- dropMenu: false
- });
-};
-
-appActions.toggleDropMenu = function(e) {
- if (e) {
- e.stopPropagation();
- }
- var dropMenu = this.app.state.dropMenu;
- this.app.setState({
- dropMenu: !dropMenu
- });
-};
-
-appActions._logMetric = function(eventName, properties) {
- api.metrics.track(eventName, properties);
-};
-
-appActions._logError = function(error, friendlyMessage, properties) {
- api.errors.log(error, friendlyMessage, properties);
-};
-
-appActions._mergeDevicesWithUploads = function(devices, uploads) {
- var self = this;
-
- // map connected devices to upload ids
- var connectedDeviceMap = _.reduce(devices, function(acc, d) {
- var upload = self._newUploadFromDevice(d);
- acc[self._getUploadId(upload)] = d;
- return acc;
- }, {});
-
- // work only on device uploads
- var deviceUploads = _.filter(uploads, function(upload) {
- return upload.source.type === 'device';
- });
-
- // mark device uploads that are disconnected
- // and add any newly connected devices at the end of the list
- var newDeviceMap = _.clone(connectedDeviceMap);
- _.forEach(deviceUploads, function(upload) {
- var uploadId = self._getUploadId(upload);
- var connectedDevice = connectedDeviceMap[uploadId];
- if (!connectedDevice) {
- upload.source.connected = false;
- delete upload.progress;
- }
- else {
- upload.source = _.assign(
- upload.source, {connected: true}, connectedDevice
- );
- delete newDeviceMap[uploadId];
- }
- });
- _.forEach(_.values(newDeviceMap), function(d) {
- deviceUploads.push(self._newUploadFromDevice(d));
- });
-
- // add back CareLink upload at beginning of list
- var carelinkUpload = _.find(uploads, function(upload) {
- return upload.source.type === 'carelink';
- });
- var newUploads = deviceUploads;
- if (carelinkUpload) {
- newUploads.unshift(carelinkUpload);
- }
-
- return newUploads;
-};
-
-appActions._getUploadId = function(upload) {
- var source = upload.source;
- if (source.type === 'device' || source.type === 'block') {
- return source.driverId;
- }
- if (source.type === 'carelink') {
- return source.type;
- }
- return null;
-};
-
-appActions._newUploadFromDevice = function(d) {
- return {
- source: _.assign({type: 'device'}, d, {connected: true})
- };
-};
-
-appActions.readFile = function(uploadIndex, targetId, file, extension) {
- var self = this;
-
- if (!file) {
- return;
- }
-
- function inputError(error) {
- self._updateUpload(uploadIndex, function(upload) {
- var now = self._now();
- var instance = {
- targetId: targetId,
- start: now,
- step: 'start',
- percentage: 0,
- finish: now,
- error: error
- };
- upload.progress = instance;
- upload = self._addToUploadHistory(upload, instance);
- return upload;
- });
- }
-
- if (file.name.slice(-extension.length) === extension) {
- var reader = new FileReader();
-
- // display filename in UI before loading
- reader.onloadstart = function() {
- self._updateUpload(uploadIndex, function(upload) {
- upload.file = {
- name: file.name
- };
- return upload;
- });
- };
-
- reader.onerror = function() {
- var error = new Error(self.errorText.E_READING_FILE + file.name);
- appActions.addMoreInfoToError(error, appActions.errorStages['E_READING_FILE']);
- inputError(error);
- // the return is just for ease of testing
- return error;
- };
-
- reader.onloadend = (function(theFile) {
- return function(e) {
- self._updateUpload(uploadIndex, function(upload) {
- upload.file.data = e.srcElement.result;
- var options = {
- filename: upload.file.name,
- filedata: upload.file.data
- };
- self.upload(uploadIndex, options);
- return upload;
- });
- };
- })(file);
-
- reader.readAsArrayBuffer(file);
-
- return true;
- }
- else {
- var error = new Error(self.errorText.E_WRONG_FILE_EXT + extension);
- appActions.addMoreInfoToError(error, appActions.errorStages['STAGE_PICK_FILE']);
- inputError(error);
- // the return is just for ease of testing
- return error;
- }
-};
-
-appActions.upload = function(uploadIndex, options, cb) {
- var self = this;
- cb = cb || _.noop;
-
- this._assertValidUploadIndex(uploadIndex);
-
- if (appState.hasUploadInProgress()) {
- var err = new Error(self.errorText.E_UPLOAD_IN_PROGRESS);
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_UPLOAD']);
- throw err;
- }
-
- var upload = this.app.state.uploads[uploadIndex];
-
- options = _.assign(options, {
- targetId: this.app.state.targetId,
- timezone: this.app.state.targetTimezone,
- progress: this._setUploadPercentage.bind(this, uploadIndex),
- version: config.namedVersion //e.g. Tidepool Uploader v0.1.0
- });
- var onUploadFinish = function(err, records) {
- if (err) {
- self._handleUploadError(uploadIndex, err);
- return cb(err);
- }
- self._handleUploadSuccess(uploadIndex, records);
- return cb(null, records);
- };
-
- this._setUploadStart(uploadIndex, options);
-
- self._logMetric(
- self.trackedState.UPLOAD_STARTED+' '+self._getUploadId(upload),
- { type: upload.source.type, source:upload.source.driverId }
- );
-
- if (upload.source.type === 'device' || upload.source.type === 'block') {
- var driverId = this.app.state.uploads[uploadIndex].source.driverId;
- return this._uploadDevice(driverId, options, onUploadFinish);
- }
- else if (upload.source.type === 'carelink') {
- var credentials = {
- username: options.username,
- password: options.password
- };
- return this._uploadCarelink(credentials, options, onUploadFinish);
- }
- else {
- var error = new Error(self.errorText.E_UNSUPPORTED_TYPE + upload.source.type);
- appActions.addMoreInfoToError(error, appActions.errorStages['STAGE_UPLOAD']);
- throw error;
- }
-};
-
-appActions._assertValidUploadIndex = function(uploadIndex) {
- if (uploadIndex > this.app.state.uploads.length - 1) {
- var err = new Error(this.errorText.E_INVALID_UPLOAD_INDEX + uploadIndex);
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_UPLOAD']);
- throw err;
- }
-};
-
-appActions._setUploadStart = function(uploadIndex, options) {
- var self = this;
- this._updateUpload(uploadIndex, function(upload) {
- var progress = {
- targetId: options.targetId,
- start: self._now(),
- step: 'start',
- percentage: 0
- };
-
- // remember CareLink username we're uploading from
- if (options.username) {
- progress.username = options.username;
- }
-
- upload.progress = progress;
- return upload;
- });
-};
-
-appActions._setUploadPercentage = function(uploadIndex, step, percentage) {
- this._updateUpload(uploadIndex, function(upload) {
- upload.progress = _.assign(upload.progress, {
- step: step,
- percentage: percentage
- });
- return upload;
- });
-};
-
-appActions._handleUploadSuccess = function(uploadIndex, records) {
- var self = this;
- this._updateUpload(uploadIndex, function(upload) {
- //log metric details
- self._logMetric(
- self.trackedState.UPLOAD_SUCCESS+' '+self._getUploadId(upload),
- { type: upload.source.type,
- source: upload.source.driverId,
- started: upload.progress.start,
- finished: upload.progress.finish,
- processed: upload.progress.count
- }
- );
-
- var instance = _.assign(upload.progress, {
- finish: self._now(),
- success: true,
- count: records.length
- });
- upload.progress = instance;
- upload = self._addToUploadHistory(upload, instance);
- if (upload.file != null) {
- delete upload.file;
- }
- return upload;
- });
-};
-
-appActions._handleUploadError = function(uploadIndex, error) {
- var self = this;
- this._updateUpload(uploadIndex, function(upload) {
-
- // log the errors
- self._logMetric(
- self.trackedState.UPLOAD_FAILED +' '+self._getUploadId(upload),
- {type: upload.source.type ,source:upload.source.driverId, error: error }
- );
-
- self._logError(
- error,
- self.trackedState.UPLOAD_FAILED +' '+self._getUploadId(upload),
- {type: upload.source.type ,source:upload.source.driverId}
- );
-
- var instance = _.assign(upload.progress, {
- finish: self._now(),
- error: error
- });
- upload.progress = instance;
- upload = self._addToUploadHistory(upload, instance);
- if (upload.file != null) {
- delete upload.file;
- }
- return upload;
- });
-
- //for this specific error we are showing the page that prompts the user to update the uploader version
- if (error.code === 'outdatedVersion') {
- this.app.setState({ page: 'error' });
- }
-};
-
-appActions._addToUploadHistory = function(upload, instance) {
- var history = upload.history || [];
- history.unshift(instance);
- upload.history = history;
- return upload;
-};
-
-appActions._updateUpload = function(uploadIndex, updateFn) {
- var uploads = this.app.state.uploads;
- uploads[uploadIndex] = updateFn(uploads[uploadIndex]);
- this.app.setState({
- uploads: uploads
- });
-};
-
-appActions._uploadDevice = function(driverId, options, cb) {
-
- var self = this;
-
- device.detect(driverId, options, function(err, d) {
- if (err) {
- var currentOS = this.app.state._os; // "mac", "win", "android", "cros", "linux", or "openbsd"
- var upload = _.findWhere(this.app.state.uploads, {source : {driverId: driverId}});
- if(upload !== undefined) {
- if((currentOS === 'mac') && upload.mac) {
- appActions.errorStages['STAGE_DRIVER'].driverLink = upload.mac.driverLink;
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DRIVER']);
- return cb(err);
- }else if((currentOS === 'win') && upload.win) {
- appActions.errorStages['STAGE_DRIVER'].driverLink = upload.win.driverLink;
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DRIVER']);
- return cb(err);
- }
- }
-
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DEVICE_DETECT']);
- return cb(err);
- }
-
- if (!d && options.filename == null) {
- err = new Error(self.errorText.E_DEVICE_NOT_CONNECTED);
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DEVICE_DETECT']);
- return cb(err);
- }
-
- device.upload(driverId, options, function(err, records) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_DEVICE_UPLOAD']);
- return cb(err);
- }
-
- records = records || [];
- return cb(null, records);
- });
- });
-};
-
-appActions._now = function() {
- return sundial.utcDateString();
-};
-
-appActions._uploadCarelink = function(credentials, options, cb) {
- var self = this;
-
- var payload = {
- targetUserId: options.targetId,
- carelinkUsername: credentials.username,
- carelinkPassword: credentials.password,
- daysAgo: config.DEFAULT_CARELINK_DAYS
- };
-
- api.upload.fetchCarelinkData(payload, function(err, data) {
-
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_CARELINK_FETCH']);
- self._logMetric(self.trackedState.CARELINK_FETCH_FAILED);
- self._logError(err, self.trackedState.CARELINK_FETCH_FAILED);
- return cb(err);
- }
-
- self._logMetric(self.trackedState.CARELINK_FETCH_SUCCESS);
-
- carelink.upload(data, options, function(err, records) {
- if (err) {
- appActions.addMoreInfoToError(err, appActions.errorStages['STAGE_CARELINK_UPLOAD']);
- return cb(err);
- }
-
- records = records || [];
- return cb(null, records);
- });
- });
-};
-
-appActions.reset = function(uploadIndex) {
- this._assertValidUploadIndex(uploadIndex);
- this._updateUpload(uploadIndex, function(upload) {
- return _.omit(upload, 'progress');
- });
-};
-
-module.exports = appActions;
diff --git a/lib/state/appState.js b/lib/state/appState.js
deleted file mode 100644
index 6f95effa6c..0000000000
--- a/lib/state/appState.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * == BSD2 LICENSE ==
- * Copyright (c) 2014, 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
- * License as published by the Open Source Initiative at opensource.org.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the License for more details.
- *
- * You should have received a copy of the License along with this program; if
- * not, you can obtain one from Tidepool Project at tidepool.org.
- * == BSD2 LICENSE ==
- */
-
-var _ = require('lodash');
-var mapcat = require('../core/mapcat');
-
-var config = require('../config');
-
-var appState = {};
-
-appState.bindApp = function(app) {
- this.app = app;
- return this;
-};
-
-appState.getInitial = function() {
-
- var uploads = [
- {
- name: 'Insulet OmniPod',
- key: 'omnipod',
- source: {type: 'block', driverId: 'InsuletOmniPod', extension: '.ibf'}
- },
- {
- name: 'Dexcom',
- key: 'dexcom',
- source: {type: 'device', driverId: 'Dexcom'},
- win: {driverLink:'http://tidepool.org/downloads/'},
- mac: {driverLink:'http://tidepool.org/downloads/'},
- },
- {
- name: 'Abbott Precision Xtra',
- key: 'precisionxtra',
- source: {type: 'device', driverId: 'AbbottPrecisionXtra'}
- },
- {
- name: 'Tandem',
- key: 'tandem',
- source: {type: 'device', driverId: 'Tandem'}
- },
- // {
- // name: 'OneTouch Ultra2',
- // key: 'onetouchultra2',
- // source: {type: 'device', driverId: 'OneTouchUltra2'}
- // },
- // {
- // name: 'OneTouch UltraMini',
- // key: 'onetouchmini',
- // source: {type: 'device', driverId: 'OneTouchMini'}
- // },
- {
- name: 'Abbott FreeStyle Lite',
- key: 'abbottfreestylelite',
- source: {type: 'device', driverId: 'AbbottFreeStyleLite'}
- },
- {
- name: 'Abbott FreeStyle Freedom Lite',
- key: 'abbottfreestylefreedomlite',
- source: {type: 'device', driverId: 'AbbottFreeStyleFreedomLite'}
- },
- {
- name: 'Bayer Contour Next',
- key: 'bayercontournext',
- source: {type: 'device', driverId: 'BayerContourNext'}
- },
- {
- name: 'Bayer Contour Next USB',
- key: 'bayercontournextusb',
- source: {type: 'device', driverId: 'BayerContourNextUsb'}
- },
- {
- name: 'Bayer Contour USB',
- key: 'bayercontourusb',
- source: {type: 'device', driverId: 'BayerContourUsb'}
- },
- {
- name: 'Bayer Contour Next LINK',
- key: 'bayercontournextlink',
- source: {type: 'device', driverId: 'BayerContourNextLink'}
- }
- ];
-
- if (config.CARELINK) {
- uploads.unshift({name: 'Medtronic (from CareLink)', key: 'carelink', source: {type: 'carelink'}});
- }
-
- return {
- dropMenu: false,
- howToUpdateKBLink: 'https://tidepool-project.helpscoutdocs.com/article/6-how-to-install-or-upgrade-the-tidepool-uploader-gen',
- page: 'loading',
- user: null,
- targetId: null,
- targetDevices: [],
- targetTimezone: null,
- targetTimezoneLabel: null,
- uploads: uploads
- };
-};
-
-appState.isLoggedIn = function() {
- return Boolean(this.app.state.user);
-};
-
-// For now we only support only one upload at a time,
-// so the "current" upload is the first one of the list "in progress"
-appState.currentUploadIndex = function() {
- return _.findIndex(this.app.state.uploads, this._isUploadInProgress);
-};
-
-appState._isUploadInProgress = function(upload) {
- if (upload.progress && !upload.progress.finish) {
- return true;
- }
- return false;
-};
-
-appState.hasUploadInProgress = function() {
- return Boolean(this.currentUploadIndex() !== -1);
-};
-
-appState.deviceCount = function() {
- return _.filter(this.app.state.uploads, function(upload) {
- return upload.source.type === 'device';
- }).length;
-};
-
-appState.uploadsWithFlags = function() {
- var self = this;
- var currentUploadIndex = this.currentUploadIndex();
- var currentUploadKey = null;
- if (currentUploadIndex !== -1) {
- currentUploadKey = this.app.state.uploads[currentUploadIndex].key;
- }
- var targetedUploads = _.filter(this.app.state.uploads, function(upload) {
- return _.contains(self.app.state.targetDevices, upload.key);
- });
- return _.map(targetedUploads, function(upload, index) {
- upload = _.clone(upload);
- var source = upload.source || {};
-
- if (currentUploadIndex !== -1 && upload.key !== currentUploadKey) {
- upload.disabled = true;
- }
- if (source.type === 'device' &&
- source.connected === false) {
- upload.disconnected = true;
- upload.disabled = true;
- }
- if (source.type === 'carelink') {
- upload.carelink = true;
- }
- if (self._isUploadInProgress(upload)) {
- upload.uploading = true;
- if (source.type === 'carelink' && upload.progress.step === 'start') {
- upload.fetchingCarelinkData = true;
- }
- }
- if (!upload.uploading && upload.progress) {
- upload.completed = true;
- var instance = upload.progress;
- if (instance.success) {
- upload.successful = true;
- }
- else if (instance.error) {
- upload.failed = true;
- upload.error = instance.error;
- }
- }
-
- return upload;
- });
-};
-
-module.exports = appState;
diff --git a/main.js b/main.js
index b49cc29c7f..8b06c16d18 100644
--- a/main.js
+++ b/main.js
@@ -100,7 +100,7 @@ function setServer(window, info) {
console.log('will use', info.menuItemId, 'server');
var serverinfo = serverdata[info.menuItemId];
- window.app.api.setHosts(serverinfo);
+ window.api.setHosts(serverinfo);
}
diff --git a/manifest.json b/manifest.json
index 99b1991247..5ed20101ad 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Tidepool Uploader",
"short_name": "Uploader",
- "version": "0.242.0",
+ "version": "0.246.0",
"description": "The Tidepool Uploader helps you get your data from insulin pumps, CGMs and BG meters into Tidepool’s secure cloud platform.",
"minimum_chrome_version": "38",
"icons": {
diff --git a/package.json b/package.json
index fc2747d91b..b2f7d88a8b 100644
--- a/package.json
+++ b/package.json
@@ -1,47 +1,74 @@
{
"name": "tidepool-uploader",
- "version": "0.242.0",
+ "version": "0.246.0",
"description": "Tidepool Project Universal Uploader",
"private": true,
"main": "main.js",
"author": "Kent Quirk",
"license": "BSD-2-Clause",
"scripts": {
- "test": "mocha test",
+ "test": "mocha test/node/ && mocha test/node/**/*.js && ./node_modules/karma/bin/karma start",
+ "browser-tests": "./node_modules/karma/bin/karma start --browsers PhantomJS,Chrome",
+ "node-tests": "mocha test/node/ && mocha test/node/**/*.js",
+ "karma-watch": "./node_modules/karma/bin/karma start --no-single-run",
"start": "bash ./scripts/config.sh && webpack -d --progress --colors --watch",
"build": "bash ./scripts/build.sh",
- "jshint": "gulp jshint",
- "jshint-watch": "gulp jshint-watch"
+ "lint": "./node_modules/.bin/eslint lib test"
},
"dependencies": {
- "async": "0.9.0",
- "babyparse": "0.4.1",
- "blueimp-md5": "1.1.0",
- "commander": "2.7.1",
- "lodash": "3.6.0",
- "node-uuid": "1.4.3",
- "react": "0.12.0",
- "react-select": "0.4.6",
+ "async": "1.5.2",
+ "babyparse": "0.4.3",
+ "blueimp-md5": "2.1.0",
+ "classnames": "2.2.3",
+ "commander": "2.9.0",
+ "lodash": "3.10.1",
+ "node-uuid": "1.4.7",
+ "react": "0.14.7",
+ "react-addons-update": "0.14.7",
+ "react-dom": "0.14.7",
+ "react-redux": "4.2.0",
+ "react-select": "0.9.1",
+ "redux": "3.2.0",
+ "redux-thunk": "1.0.3",
+ "semver": "5.1.0",
"stack-trace": "0.0.9",
- "sundial": "1.4.0",
+ "sundial": "1.5.1",
"tidepool-platform-client": "0.22.0"
},
"devDependencies": {
- "css-loader": "0.9.1",
- "d3": "3.5.5",
- "gulp": "3.8.11",
- "gulp-jshint": "1.9.2",
- "gulp-react": "3.0.0",
- "jshint-stylish": "1.0.1",
- "json-loader": "0.5.1",
- "jsx-loader": "0.12.2",
- "less": "2.4.0",
- "less-loader": "2.1.0",
- "merge-stream": "0.1.7",
- "mocha": "2.2.1",
- "proxyquire": "1.4.0",
+ "babel-core": "5.8.34",
+ "babel-eslint": "5.0.0-beta10",
+ "babel-loader": "5.3.3",
+ "babel-plugin-rewire": "0.1.22",
+ "babel-runtime": "5.8.34",
+ "chai": "3.5.0",
+ "css-loader": "0.23.1",
+ "d3": "3.5.14",
+ "eslint": "1.10.3",
+ "eslint-plugin-react": "3.16.1",
+ "flux-standard-action": "0.6.0",
+ "json-loader": "0.5.4",
+ "karma": "0.13.19",
+ "karma-chai": "0.1.0",
+ "karma-chrome-launcher": "0.2.2",
+ "karma-mocha": "0.2.1",
+ "karma-mocha-reporter": "1.1.5",
+ "karma-phantomjs-launcher": "1.0.0",
+ "karma-webpack": "1.7.0",
+ "less": "2.6.0",
+ "less-loader": "2.2.2",
+ "merge-stream": "1.0.0",
+ "mocha": "2.4.5",
+ "phantomjs-prebuilt": "2.1.3",
+ "proxyquire": "1.7.4",
+ "react-addons-test-utils": "0.14.7",
+ "redux-devtools": "3.0.2",
+ "redux-devtools-dock-monitor": "1.0.1",
+ "redux-devtools-log-monitor": "1.0.2",
+ "redux-logger": "2.4.0",
+ "redux-mock-store": "0.0.6",
"salinity": "0.0.8",
- "style-loader": "0.8.3",
- "webpack": "1.7.2"
+ "style-loader": "0.13.0",
+ "webpack": "1.12.12"
}
}
diff --git a/scripts/build.sh b/scripts/build.sh
index ac39e46e22..f2161dd56d 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -1,7 +1,7 @@
# Build zip file for Chrome Web Store
rm -rf dist.zip
rm -rf dist
-node_modules/.bin/webpack -p --progress --colors --output-path dist/build
+NODE_ENV=production node_modules/.bin/webpack -p --progress --colors --output-path dist/build
node scripts/cat-config.js > dist/build/config.js
cp manifest.json dist/
cp main.js dist/
diff --git a/styles/components/App.less b/styles/components/App.less
index 0628178929..3294e125de 100644
--- a/styles/components/App.less
+++ b/styles/components/App.less
@@ -13,6 +13,10 @@
* not, you can obtain one from Tidepool Project at tidepool.org.
*/
+.App {
+ height: 100%;
+}
+
// Header
// ====================================
@@ -39,13 +43,12 @@
@App-mainPageBorder: 1px;
-.App--main .App-page, .App--settings .App-page, .App--error .App-page {
+.App--main .App-page, .App--settings .App-page {
width: 550px;
background-color: rgba(255, 255, 255, 0.75);
border: @App-mainPageBorder solid @gray-border;
border-radius: 2px;
padding: 15px (30px - @App-mainPageBorder) 25px;
- margin-bottom: 10px;
}
.App-signup {
@@ -70,7 +73,8 @@
}
.App-footer {
- margin-top: 20px;
+ margin-top: 10px;
+ margin-bottom: 20px;
background: url(/images/tidepool_logo_transparent.png) 50px 5px no-repeat;
display: flex;
align-items: flex-end;
diff --git a/styles/components/Scan.less b/styles/components/DevTools.less
similarity index 81%
rename from styles/components/Scan.less
rename to styles/components/DevTools.less
index 029850757b..2f04a2e409 100644
--- a/styles/components/Scan.less
+++ b/styles/components/DevTools.less
@@ -13,13 +13,16 @@
* not, you can obtain one from Tidepool Project at tidepool.org.
*/
-.Scan-status {
- font-size: @font-size-large;
- line-height: 1.5;
- padding-top: 20px;
- padding-bottom: 20px;
+.DevTools-container {
+ width: 100%;
+ height: 100%;
}
-.Scan-status--error {
- color: @red;
-}
+.DevTools * {
+ div {
+ display: block;
+ }
+ span {
+ display: inline;
+ }
+}
\ No newline at end of file
diff --git a/styles/components/LoggedInAs.less b/styles/components/LoggedInAs.less
index 09dcb510f8..5da3aadb88 100644
--- a/styles/components/LoggedInAs.less
+++ b/styles/components/LoggedInAs.less
@@ -44,6 +44,10 @@ a.LoggedInAs-link {
margin-right: 1px;
}
+ &[disabled] {
+ cursor: not-allowed;
+ }
+
.interactive;
}
diff --git a/styles/components/TimezoneSelection.less b/styles/components/TimezoneDropdown.less
similarity index 53%
rename from styles/components/TimezoneSelection.less
rename to styles/components/TimezoneDropdown.less
index 08eec3ea67..9a09806a44 100644
--- a/styles/components/TimezoneSelection.less
+++ b/styles/components/TimezoneDropdown.less
@@ -1,19 +1,19 @@
-.TimezoneSelection {
+.TimezoneDropdown {
border-bottom: 1px solid @gray-border;
}
-.TimezoneSelection-timezone {
+.TimezoneDropdown-timezone {
margin: 10px 40px 10px 40px;
}
-.TimezoneSelection-timezone--label {
+.TimezoneDropdown-timezone--label {
font-size: @font-size-large;
}
-.TimezoneSelection-timezone--list {
+.TimezoneDropdown-timezone--list {
margin-top: -30px;
- &.TimezoneSelection--settings {
+ &.TimezoneDropdown--settings {
margin-left: 150px;
width: 260px;
}
diff --git a/styles/components/UpdatePlease.less b/styles/components/UpdatePlease.less
deleted file mode 100644
index ebc32d0b0d..0000000000
--- a/styles/components/UpdatePlease.less
+++ /dev/null
@@ -1,21 +0,0 @@
-.UpdatePlease {
- .center-container;
-
- a {
- .blue-link;
- }
-
- p {
- font-size: 1.2em;
- text-align: center;
- // our box-flex mixin wreaked havoc here and was making spans display flex
- // (effectively block)
- // TODO: fix that at the source
- span {
- display: inline;
- }
- &.most-important {
- font-weight: bold;
- }
- }
-}
diff --git a/styles/components/Upload.less b/styles/components/Upload.less
index 69718db7bf..5b29e1e0b0 100644
--- a/styles/components/Upload.less
+++ b/styles/components/Upload.less
@@ -134,18 +134,28 @@ input[type=file] {
height: 0;
position: absolute;
}
-// replace with a button consistent with rest of app
.Upload-fileinput::before {
visibility: visible;
content: 'Choose file';
.btn;
- .btn-secondary;
+ .btn-primary;
+}
+
+// this is some shame.css right here
+// this style is part of the btn-primary mixin
+// but neither the selector nor the transparency work with the file input
+.Upload-fileinput[disabled]::before,
+.Upload-fileinput[disabled]:hover::before,
+.Upload-fileinput[disabled]:focus::before,
+.Upload-fileinput[disabled]:active::before {
+ cursor: not-allowed;
+ background-color: rgb(211, 208, 218);
}
.Upload-fileinput:hover::before,
.Upload-fileinput:focus::before,
.Upload-fileinput:active::before {
- border-color: @purple-light;
+ background-color: @purple-light;
}
.Upload-helpText {
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/styles/components/UploadSettings.less b/styles/components/UserDropdown.less
similarity index 55%
rename from styles/components/UploadSettings.less
rename to styles/components/UserDropdown.less
index b56a79dfe6..d993dc66f0 100644
--- a/styles/components/UploadSettings.less
+++ b/styles/components/UserDropdown.less
@@ -1,6 +1,4 @@
-.UploadSettings {}
-
-.UploadSettings-uploadGroup {
+.UserDropdown-uploadGroup {
margin: 20px 40px 10px 40px;
select {
@@ -8,19 +6,19 @@
}
}
-.UploadSettings-uploadGroup--label {
+.UserDropdown-uploadGroup--label {
font-size: @font-size-large;
}
-.UploadSettings-uploadGroup--list {
+.UserDropdown-uploadGroup--list {
margin-top: -30px;
- &.UploadSettings--settings {
+ &.UserDropdown--settings {
margin-left: 150px;
width: 260px;
}
- &.UploadSettings--main {
+ &.UserDropdown--main {
margin-left: 130px;
width: 280px;
}
diff --git a/styles/components/VersionCheck.less b/styles/components/VersionCheck.less
new file mode 100644
index 0000000000..4b89c0c64b
--- /dev/null
+++ b/styles/components/VersionCheck.less
@@ -0,0 +1,80 @@
+.purple-centered {
+ color: @purple-dark;
+ text-align: center;
+}
+
+.VersionCheck {
+ .mostly-opaque-background {
+ background-color: rgba(255, 255, 255, 0.925);
+ }
+ &.VersionCheck--failed {
+ background: url(/images/unhappy_meter.png) center center / 69px 166px no-repeat;
+ .mostly-opaque-background();
+ }
+ &.VersionCheck--offline {
+ background: url(/images/unhappy_device.png);
+ background-position: 50% 60%;
+ background-size: 105px 237px;
+ background-repeat: no-repeat;
+ .mostly-opaque-background();
+ }
+ &.VersionCheck--outdated {
+ background: url(/images/happy_device.png);
+ background-position: 50% 60%;
+ background-size: 156px 213px;
+ background-repeat: no-repeat;
+ .mostly-opaque-background();
+ }
+
+ position: absolute;
+ top: 0; left: 0;
+
+ width: 100%;
+ height: @app-height;
+
+ .VersionCheck-text {
+ min-height: 425px;
+ justify-content: center;
+
+ a {
+ color: @purple-light;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: @purple-medium;
+ text-decoration: none;
+ }
+ }
+
+ p {
+ margin: 0;
+ .purple-centered();
+ font-size: @font-size-large;
+ font-weight: bold;
+ // our box-flex mixin wreaked havoc here and was making spans display flex
+ // (effectively block)
+ // TODO: fix that at the source
+ span {
+ display: inline;
+ }
+ &.most-important {
+ margin-top: 20px;
+ }
+ }
+ }
+}
+
+.VersionCheck-error {
+ position: absolute;
+ top: 600px; left: 0px;
+
+ width: 100%;
+ min-height: 80px;
+
+ p {
+ margin: 0;
+ .purple-centered();
+ font-size: @font-size-small;
+ }
+}
diff --git a/styles/core/buttons.less b/styles/core/buttons.less
index cde7239367..a5346001e6 100644
--- a/styles/core/buttons.less
+++ b/styles/core/buttons.less
@@ -48,7 +48,6 @@ a.btn {
&[disabled],
fieldset[disabled] & {
cursor: not-allowed;
- pointer-events: none; // Future-proof disabling of clicks
opacity: .65;
}
}
@@ -59,7 +58,7 @@ a.btn {
// Main action button style
.btn-primary,
a.btn-primary {
- background-color: #291647;
+ background-color: @purple-dark;
color: #fff;
&:hover,
@@ -71,7 +70,7 @@ a.btn-primary {
&.disabled,
&[disabled],
fieldset[disabled] & {
- background-color: fade(@purple-light, 30%);
+ background-color: fade(@purple-dark, 30%);
}
}
@@ -87,16 +86,3 @@ a.btn-secondary {
border-color: @purple-light;
}
}
-
-.btn-tertiary,
-a.btn-tertiary {
- color: @text-color;
- background-color: #fff;
- border-color: fade(@red, 50%);
-
- &:hover,
- &:focus,
- &:active {
- border-color: @red;
- }
-}
diff --git a/styles/core/dropdowns.less b/styles/core/dropdowns.less
index 24e522496b..92a689d5ca 100644
--- a/styles/core/dropdowns.less
+++ b/styles/core/dropdowns.less
@@ -6,7 +6,6 @@
display: block;
}
-
/**
* React Select
* ============
@@ -17,19 +16,32 @@
.Select {
position: relative;
}
-.Select-control {
- position: relative;
- overflow: hidden;
- background-color: white;
- border: 1px solid #cccccc;
- border-color: #d9d9d9 #cccccc #b3b3b3;
- border-radius: 4px;
+.Select,
+.Select div,
+.Select input,
+.Select span {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
box-sizing: border-box;
- color: #333333;
+}
+.Select.is-disabled > .Select-control {
+ background-color: #f6f6f6;
+}
+.Select.is-disabled .Select-arrow-zone {
cursor: default;
+ pointer-events: none;
+}
+.Select-control {
+ background-color: #fff;
+ .form-control-border();
+ color: #333;
+ cursor: default;
+ display: table;
+ height: 36px;
outline: none;
- padding: 8px 52px 8px 10px;
- transition: all 200ms ease;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
}
.Select-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
@@ -40,44 +52,67 @@
.is-open > .Select-control {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
- background: white;
- border-color: #b3b3b3 #cccccc #d9d9d9;
+ background: #fff;
+ border-color: #b3b3b3 #ccc #d9d9d9;
}
.is-open > .Select-control > .Select-arrow {
- border-color: transparent transparent #999999;
+ border-color: transparent transparent #999;
border-width: 0 5px 5px;
}
.is-searchable.is-focused:not(.is-open) > .Select-control {
cursor: text;
}
-.is-focused:not(.is-open) > .Select-control {
- border-color: #0088cc #0099e6 #0099e6;
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
-}
.Select-placeholder {
- color: #aaaaaa;
- padding: 8px 52px 8px 10px;
+ bottom: 0;
+ color: #aaa;
+ left: 0;
+ line-height: 34px;
+ padding-left: 10px;
+ padding-right: 10px;
position: absolute;
+ right: 0;
top: 0;
- left: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value > .Select-control > .Select-placeholder {
- color: #333333;
+ color: #333;
+}
+.Select-value {
+ color: #aaa;
+ left: 0;
+ padding: 8px 52px 8px 10px;
+ position: absolute;
+ right: -15px;
+ top: 0;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.has-value > .Select-control > .Select-value {
+ color: #333;
+}
+.Select-input {
+ height: 34px;
+ padding-left: 10px;
+ padding-right: 10px;
+ vertical-align: middle;
}
.Select-input > input {
- cursor: default;
background: none transparent;
border: 0 none;
+ box-shadow: none;
+ cursor: default;
+ display: inline-block;
font-family: inherit;
font-size: inherit;
+ height: 34px;
margin: 0;
- padding: 0;
outline: none;
- display: inline-block;
+ padding: 0;
-webkit-appearance: none;
}
.is-focused .Select-input > input {
@@ -86,60 +121,93 @@
.Select-control:not(.is-searchable) > .Select-input {
outline: none;
}
+.Select-loading-zone {
+ cursor: pointer;
+ display: table-cell;
+ position: relative;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+}
.Select-loading {
- -webkit-animation: spin 400ms infinite linear;
- -o-animation: spin 400ms infinite linear;
- animation: spin 400ms infinite linear;
+ -webkit-animation: Select-animation-spin 400ms infinite linear;
+ -o-animation: Select-animation-spin 400ms infinite linear;
+ animation: Select-animation-spin 400ms infinite linear;
width: 16px;
height: 16px;
box-sizing: border-box;
border-radius: 50%;
- border: 2px solid #cccccc;
- border-right-color: #333333;
+ border: 2px solid #ccc;
+ border-right-color: #333;
display: inline-block;
position: relative;
- margin-top: -8px;
- position: absolute;
- right: 30px;
- top: 50%;
+ vertical-align: middle;
+}
+.Select-clear-zone {
+ -webkit-animation: Select-animation-fadeIn 200ms;
+ -o-animation: Select-animation-fadeIn 200ms;
+ animation: Select-animation-fadeIn 200ms;
+ color: #999;
+ cursor: pointer;
+ display: table-cell;
+ position: relative;
+ text-align: center;
+ vertical-align: middle;
+ width: 17px;
}
-.has-value > .Select-control > .Select-loading {
- right: 46px;
+.Select-clear-zone:hover {
+ color: #D0021B;
}
.Select-clear {
- color: #999999;
- cursor: pointer;
display: inline-block;
- font-size: 16px;
- padding: 6px 10px;
- position: absolute;
- right: 17px;
- top: 0;
+ font-size: 18px;
+ line-height: 1;
}
-.Select-clear:hover {
- color: #c0392b;
+.Select--multi .Select-clear-zone {
+ width: 17px;
}
-.Select-clear > span {
- font-size: 1.1em;
+.Select-arrow-zone {
+ cursor: pointer;
+ display: table-cell;
+ position: relative;
+ text-align: center;
+ vertical-align: middle;
+ width: 25px;
+ padding-right: 5px;
}
.Select-arrow {
- border-color: #999999 transparent transparent;
+ border-color: #999 transparent transparent;
border-style: solid;
- border-width: 5px 5px 0;
- content: " ";
- display: block;
+ border-width: 5px 5px 2.5px;
+ display: inline-block;
height: 0;
- margin-top: -ceil(2.5px);
- position: absolute;
- right: 10px;
- top: 14px;
width: 0;
}
+.is-open .Select-arrow,
+.Select-arrow-zone:hover > .Select-arrow {
+ border-top-color: #666;
+}
+@-webkit-keyframes Select-animation-fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes Select-animation-fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
.Select-menu-outer {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
- background-color: white;
- border: 1px solid #cccccc;
+ background-color: #fff;
+ border: 1px solid #ccc;
border-top-color: #e6e6e6;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
@@ -168,32 +236,39 @@
}
.Select-option.is-focused {
background-color: #f2f9fc;
- color: #333333;
+ color: #333;
+}
+.Select-option.is-disabled {
+ color: #cccccc;
+ cursor: not-allowed;
}
-.Select-noresults {
+.Select-noresults,
+.Select-search-prompt,
+.Select-searching {
box-sizing: border-box;
color: #999999;
cursor: default;
display: block;
padding: 8px 10px;
}
-.Select.is-multi .Select-control {
- padding: 2px 52px 2px 3px;
-}
-.Select.is-multi .Select-input {
+.Select--multi .Select-input {
vertical-align: middle;
- border: 1px solid transparent;
- margin: 2px;
- padding: 3px 0;
+ margin-left: 10px;
+ padding: 0;
+}
+.Select--multi.has-value .Select-input {
+ margin-left: 5px;
}
.Select-item {
background-color: #f2f9fc;
border-radius: 2px;
border: 1px solid #c9e6f2;
- color: #0088cc;
+ color: #08c;
display: inline-block;
- font-size: 1em;
- margin: 2px;
+ font-size: 0.9em;
+ margin-left: 5px;
+ margin-top: 5px;
+ vertical-align: top;
}
.Select-item-icon,
.Select-item-label {
@@ -201,13 +276,13 @@
vertical-align: middle;
}
.Select-item-label {
- cursor: default;
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
- padding: 3px 5px;
+ cursor: default;
+ padding: 2px 5px;
}
.Select-item-label .Select-item-label__a {
- color: #0088cc;
+ color: #08c;
cursor: pointer;
}
.Select-item-icon {
@@ -215,7 +290,7 @@
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid #c9e6f2;
- padding: 2px 5px 4px;
+ padding: 1px 5px 3px;
}
.Select-item-icon:hover,
.Select-item-icon:focus {
@@ -225,13 +300,27 @@
.Select-item-icon:active {
background-color: #c9e6f2;
}
-@keyframes spin {
+.Select--multi.is-disabled .Select-item {
+ background-color: #f2f2f2;
+ border: 1px solid #d9d9d9;
+ color: #888;
+}
+.Select--multi.is-disabled .Select-item-icon {
+ cursor: not-allowed;
+ border-right: 1px solid #d9d9d9;
+}
+.Select--multi.is-disabled .Select-item-icon:hover,
+.Select--multi.is-disabled .Select-item-icon:focus,
+.Select--multi.is-disabled .Select-item-icon:active {
+ background-color: #f2f2f2;
+}
+@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
}
}
-@-webkit-keyframes spin {
+@-webkit-keyframes Select-animation-spin {
to {
-webkit-transform: rotate(1turn);
}
-}
\ No newline at end of file
+}
diff --git a/styles/core/forms.less b/styles/core/forms.less
index 7c60b70c03..1de0cd54df 100644
--- a/styles/core/forms.less
+++ b/styles/core/forms.less
@@ -20,20 +20,9 @@
@form-control-height: (30px + 2*@form-control-border-size);
@form-control-border-color: @purple-light;
-.form-control {
- display: block;
-
- font-size: @font-size-base;
- height: @form-control-height;
- line-height: (@line-height-base - 2*@form-control-border-size);
- vertical-align: middle;
-
- padding: 0 15px;
-
+.form-control-border {
border: @form-control-border-size solid fade(@form-control-border-color, 50%);
border-radius: 0px;
- background: #fff;
- color: @text-color;
&:hover {
border-color: @form-control-border-color;
@@ -49,6 +38,21 @@
border-color: fade(@form-control-border-color, 70%);
cursor: not-allowed;
}
+}
+
+.form-control {
+ display: block;
+
+ font-size: @font-size-base;
+ height: @form-control-height;
+ line-height: (@line-height-base - 2*@form-control-border-size);
+ vertical-align: middle;
+
+ padding: 0 15px;
+
+ .form-control-border();
+ background: #fff;
+ color: @text-color;
textarea& {
height: auto;
diff --git a/styles/core/mixins.less b/styles/core/mixins.less
index 8c1f9e98d1..b19980e71a 100644
--- a/styles/core/mixins.less
+++ b/styles/core/mixins.less
@@ -40,15 +40,3 @@
min-height: 300px;
justify-content: center;
}
-
-// a common link style
-.blue-link {
- color: lighten(@purple-dark, 20%);
-
- &:hover,
- &:focus,
- &:active {
- color: @blue;
- text-decoration: none;
- }
-}
diff --git a/styles/core/scaffolding.less b/styles/core/scaffolding.less
index 2db4e76394..803efc7aa9 100644
--- a/styles/core/scaffolding.less
+++ b/styles/core/scaffolding.less
@@ -33,6 +33,10 @@ body {
background-color: @body-bg;
}
+#app {
+ height: @app-height;
+}
+
// Inputs
// ====================================
diff --git a/styles/core/variables.less b/styles/core/variables.less
index 811436ae2f..9f95db71a8 100644
--- a/styles/core/variables.less
+++ b/styles/core/variables.less
@@ -16,7 +16,8 @@
// Colors
// ====================================
-@purple-dark: rgb(40, 25, 70);
+@purple-dark: rgb(40, 23, 71);
+@purple-medium: rgb(123, 108, 147);
@purple-light: rgb(100, 118, 255);
@blue: rgb(98, 124, 255);
@red: rgb(247, 45, 45);
@@ -47,4 +48,7 @@
// Dimensions
// ====================================
+
+// defined as the app height in main.js
+@app-height: 710px;
@groups-dropdown-height: 72px;
diff --git a/styles/main.less b/styles/main.less
index bd6f7345c4..eaf11614bb 100644
--- a/styles/main.less
+++ b/styles/main.less
@@ -29,16 +29,16 @@
// Components
// ====================================
+@import "components/DevTools.less";
@import "components/App.less";
@import "components/DeviceSelection.less";
@import "components/LoadingBar.less";
@import "components/LoggedInAs.less";
@import "components/Login.less";
@import "components/ProgressBar.less";
-@import "components/Scan.less";
-@import "components/TimezoneSelection.less";
-@import "components/UpdatePlease.less";
+@import "components/TimezoneDropdown.less";
@import "components/Upload.less";
@import "components/UploadList.less";
-@import "components/UploadSettings.less";
+@import "components/UserDropdown.less";
+@import "components/VersionCheck.less";
@import "components/ViewDataLink.less";
diff --git a/test.config.js b/test.config.js
new file mode 100644
index 0000000000..a8d49b98d8
--- /dev/null
+++ b/test.config.js
@@ -0,0 +1,29 @@
+var path = require('path');
+var webpack = require('webpack');
+
+var definePlugin = new webpack.DefinePlugin({
+ __TEST__: true
+});
+
+module.exports = {
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules)/,
+ loader: 'babel-loader?optional=runtime&plugins=babel-plugin-rewire'
+ }, {
+ test: /\.jsx$/,
+ exclude: /(node_modules)/,
+ loader: 'babel-loader?optional=runtime&plugins=babel-plugin-rewire'
+ },
+ { test: /\.json$/, loader: 'json' }
+ ]
+ },
+ plugins: [
+ definePlugin
+ ],
+ // to fix the 'broken by design' issue with npm link-ing modules
+ resolve: { fallback: path.join(__dirname, 'node_modules') },
+ resolveLoader: { fallback: path.join(__dirname, 'node_modules') }
+};
diff --git a/test/.jshintrc b/test/.jshintrc
deleted file mode 100644
index e4f110caad..0000000000
--- a/test/.jshintrc
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "browser": true,
- "esnext": true,
- "eqnull": true,
- "globalstrict": true,
- "indent": 2,
- "laxcomma": true,
- "loopfunc": true,
- "node": true,
- "quotmark": true,
- "smarttabs": false,
- "strict": false,
- "sub": true,
- "trailing": true,
- "undef": true,
-
- "globals": {
- "describe": false,
- "it": false,
- "before": false,
- "beforeEach": false,
- "after": false,
- "afterEach": false
- },
- "expr": true
-}
diff --git a/test/browser/fixtures/nonpwd.json b/test/browser/fixtures/nonpwd.json
new file mode 100644
index 0000000000..45c8ff2d76
--- /dev/null
+++ b/test/browser/fixtures/nonpwd.json
@@ -0,0 +1,54 @@
+{
+ "user": {
+ "userid": "9508844d",
+ "username": "futejok@foobar.com",
+ "termsAccepted": "2015-12-05T13:09:50-08:00",
+ "emails": [
+ "futejok@foobar.com"
+ ]
+ },
+ "profile": {
+ "fullName": "Jeffrey Gene Lyons"
+ },
+ "memberships": [
+ {
+ "userid": "9508844d",
+ "profile": {
+ "fullName": "Jeffrey Gene Lyons"
+ }
+ },
+ {
+ "userid": "01188072",
+ "profile": {
+ "fullName": "Lily Garza",
+ "patient": {
+ "about": "Jewsein haszuk suspogpid ejeew zeiheegu.",
+ "birthday": "1978-01-13",
+ "diagnosisDate": "1988-01-22"
+ }
+ }
+ },
+ {
+ "userid": "53ef303b",
+ "profile": {
+ "fullName": "Birdie Luella Jones",
+ "patient": {
+ "about": "Wu jeohe siniece lawbe wukhuw.",
+ "birthday": "1964-01-02",
+ "diagnosisDate": "1977-01-13"
+ }
+ }
+ },
+ {
+ "userid": "cbccfb03",
+ "profile": {
+ "fullName": "Douglas Ruiz",
+ "patient": {
+ "about": "Ge ruti peva heranadi zaotcu.",
+ "birthday": "1969-08-12",
+ "diagnosisDate": "1977-08-13"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/browser/fixtures/pwd.json b/test/browser/fixtures/pwd.json
new file mode 100644
index 0000000000..ddaac331a8
--- /dev/null
+++ b/test/browser/fixtures/pwd.json
@@ -0,0 +1,42 @@
+{
+ "user": {
+ "userid": "ad7f18a8",
+ "username": "ik@foobar.com",
+ "termsAccepted": "2015-12-05T12:57:51-08:00",
+ "emails": [
+ "ik@foobar.com"
+ ]
+ },
+ "profile": {
+ "fullName": "Tillie Andrews",
+ "patient": {
+ "about": "Ti agopovtu tadbuhto map bacosub.",
+ "birthday": "1974-10-30",
+ "diagnosisDate": "1986-11-06"
+ }
+ },
+ "memberships": [
+ {
+ "userid": "ad7f18a8",
+ "profile": {
+ "fullName": "Tillie Andrews",
+ "patient": {
+ "about": "Ti agopovtu tadbuhto map bacosub.",
+ "birthday": "1974-10-30",
+ "diagnosisDate": "1986-11-06"
+ }
+ }
+ },
+ {
+ "userid": "2166340f",
+ "profile": {
+ "fullName": "George Taylor",
+ "patient": {
+ "about": "",
+ "birthday": "1977-10-12",
+ "diagnosisDate": "1989-10-14"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/browser/polyfill/function.prototype.bind.js b/test/browser/polyfill/function.prototype.bind.js
new file mode 100644
index 0000000000..c0aaa5510f
--- /dev/null
+++ b/test/browser/polyfill/function.prototype.bind.js
@@ -0,0 +1,35 @@
+(function() {
+
+var Ap = Array.prototype;
+var slice = Ap.slice;
+var Fp = Function.prototype;
+
+if (!Fp.bind) {
+ // PhantomJS doesn't support Function.prototype.bind natively, so
+ // polyfill it whenever this module is required.
+ Fp.bind = function(context) {
+ var func = this;
+ var args = slice.call(arguments, 1);
+
+ function bound() {
+ var invokedAsConstructor = func.prototype && (this instanceof func);
+ return func.apply(
+ // Ignore the context parameter when invoking the bound function
+ // as a constructor. Note that this includes not only constructor
+ // invocations using the new keyword but also calls to base class
+ // constructors such as BaseClass.call(this, ...) or super(...).
+ !invokedAsConstructor && context || this,
+ args.concat(slice.call(arguments))
+ );
+ }
+
+ // The bound function must share the .prototype of the unbound
+ // function so that any object created by one constructor will count
+ // as an instance of both constructors.
+ bound.prototype = func.prototype;
+
+ return bound;
+ };
+}
+
+})();
\ No newline at end of file
diff --git a/test/browser/redux/actions/async.test.js b/test/browser/redux/actions/async.test.js
new file mode 100644
index 0000000000..ad9af32c6a
--- /dev/null
+++ b/test/browser/redux/actions/async.test.js
@@ -0,0 +1,2019 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2014, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/*eslint-env mocha*/
+
+import _ from 'lodash';
+import { isFSA } from 'flux-standard-action';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
+import * as actionSources from '../../../../lib/redux/constants/actionSources';
+import * as actionTypes from '../../../../lib/redux/constants/actionTypes';
+import * as metrics from '../../../../lib/redux/constants/metrics';
+import { pages, steps, urls } from '../../../../lib/redux/constants/otherConstants';
+import { errorText, UnsupportedError } from '../../../../lib/redux/utils/errors';
+
+import * as asyncActions from '../../../../lib/redux/actions/async';
+import { getLoginErrorMessage, getLogoutErrorMessage } from '../../../../lib/redux/utils/errors';
+
+let pwd = require('../../fixtures/pwd.json');
+let nonpwd = require('../../fixtures/nonpwd.json');
+
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+
+global.chrome = {
+ contextMenus: {
+ removeAll: _.noop
+ },
+ runtime: {
+ getManifest: function() { return {permissions: [{usbDevices: [{driverId: '12345'}]}]}; },
+ getPlatformInfo: function(cb) { return cb({os: 'test'}); }
+ }
+};
+
+describe('Asynchronous Actions', () => {
+ afterEach(function() {
+ // very important to do this in an afterEach than in each test when __Rewire__ is used
+ // if you try to reset within each test you'll make it impossible for tests to fail!
+ asyncActions.__ResetDependency__('services');
+ });
+
+ describe('doAppInit [no session token in local storage]', () => {
+ it('should dispatch SET_VERSION, INIT_APP_REQUEST, SET_OS, HIDE_UNAVAILABLE_DEVICES, SET_FORGOT_PASSWORD_URL, SET_SIGNUP_URL, SET_PAGE, INIT_APP_SUCCESS, VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS actions', (done) => {
+ const config = {
+ os: 'test',
+ version: '0.100.0',
+ API_URL: 'http://www.acme.com'
+ };
+ const servicesToInit = {
+ api: {
+ init: (cb) => { cb(); },
+ makeBlipUrl: (path) => {
+ return 'http://www.acme.com' + path;
+ },
+ setHosts: _.noop,
+ upload: {
+ getVersions: (cb) => { cb(null, {uploaderMinimum: config.version}); }
+ }
+ },
+ carelink: {
+ init: (opts, cb) => { cb(); }
+ },
+ device: {
+ init: (opts, cb) => { cb(); }
+ },
+ localStore: {
+ init: (opts, cb) => { cb(); },
+ getInitialState: _.noop
+ },
+ log: _.noop
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.INIT_APP_REQUEST,
+ meta: {source: actionSources[actionTypes.INIT_APP_REQUEST]}
+ },
+ {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'test'},
+ meta: {source: actionSources[actionTypes.HIDE_UNAVAILABLE_DEVICES]}
+ },
+ {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: {url: 'http://www.acme.com/request-password-from-uploader'},
+ meta: {source: actionSources[actionTypes.SET_FORGOT_PASSWORD_URL]}
+ },
+ {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: {url: 'http://www.acme.com/signup'},
+ meta: {source: actionSources[actionTypes.SET_SIGNUP_URL]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.LOGIN},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ },
+ {
+ type: actionTypes.INIT_APP_SUCCESS,
+ meta: {source: actionSources[actionTypes.INIT_APP_SUCCESS]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ }
+ ];
+ asyncActions.__Rewire__('versionInfo', {
+ semver: config.version
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doAppInit(config, servicesToInit));
+ });
+ });
+
+ describe('doAppInit [with session token in local storage]', () => {
+ it('should dispatch SET_VERSION, INIT_APP_REQUEST, SET_OS, HIDE_UNAVAILABLE_DEVICES, SET_FORGOT_PASSWORD_URL, SET_SIGNUP_URL, INIT_APP_SUCCESS, VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, SET_USER_INFO_FROM_TOKEN, SET_BLIP_VIEW_DATA_URL, RETRIEVING_USERS_TARGETS, SET_PAGE actions', (done) => {
+ const config = {
+ os: 'test',
+ version: '0.100.0',
+ API_URL: 'http://www.acme.com'
+ };
+ const servicesToInit = {
+ api: {
+ init: (cb) => { cb(null, {token: 'iAmAToken'}); },
+ makeBlipUrl: (path) => {
+ return 'http://www.acme.com' + path;
+ },
+ setHosts: _.noop,
+ upload: {
+ getVersions: (cb) => { cb(null, {uploaderMinimum: config.version}); }
+ },
+ user: {
+ account: (cb) => { cb(null, pwd.user); },
+ profile: (cb) => { cb(null, pwd.profile); },
+ getUploadGroups: (cb) => { cb(null, pwd.memberships); }
+ }
+ },
+ carelink: {
+ init: (opts, cb) => { cb(); }
+ },
+ device: {
+ init: (opts, cb) => { cb(); }
+ },
+ localStore: {
+ init: (opts, cb) => { cb(); },
+ getInitialState: _.noop,
+ getItem: () => null
+ },
+ log: _.noop
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.INIT_APP_REQUEST,
+ meta: {source: actionSources[actionTypes.INIT_APP_REQUEST]}
+ },
+ {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'test'},
+ meta: {source: actionSources[actionTypes.HIDE_UNAVAILABLE_DEVICES]}
+ },
+ {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: {url: 'http://www.acme.com/request-password-from-uploader'},
+ meta: {source: actionSources[actionTypes.SET_FORGOT_PASSWORD_URL]}
+ },
+ {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: {url: 'http://www.acme.com/signup'},
+ meta: {source: actionSources[actionTypes.SET_SIGNUP_URL]}
+ },
+ {
+ type: actionTypes.INIT_APP_SUCCESS,
+ meta: {source: actionSources[actionTypes.INIT_APP_SUCCESS]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: {user: pwd.user, profile: pwd.profile, memberships: pwd.memberships},
+ meta: {source: actionSources[actionTypes.SET_USER_INFO_FROM_TOKEN]}
+ },
+ {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: {url: `http://www.acme.com/patients/${pwd.user.userid}/data`},
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ },
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.SETTINGS},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('versionInfo', {
+ semver: config.version
+ });
+ const state = {
+ uploadTargetUser: pwd.user.userid
+ };
+ const store = mockStore(state, expectedActions, done);
+ store.dispatch(asyncActions.doAppInit(config, servicesToInit));
+ });
+ });
+
+ describe('doAppInit [with error in api init]', () => {
+ it('should dispatch SET_VERSION, INIT_APP_REQUEST, SET_OS, HIDE_UNAVAILABLE_DEVICES, INIT_APP_FAILURE actions', (done) => {
+ const config = {
+ os: 'test',
+ version: '0.100.0',
+ API_URL: 'http://www.acme.com/'
+ };
+ const servicesToInit = {
+ api: {
+ init: (cb) => { cb('Error!'); },
+ makeBlipUrl: (path) => {
+ return 'http://www.acme.com/' + path;
+ },
+ setHosts: _.noop
+ },
+ carelink: {
+ init: (opts, cb) => { cb(); }
+ },
+ device: {
+ init: (opts, cb) => { cb(); }
+ },
+ localStore: {
+ init: (opts, cb) => { cb(); },
+ getInitialState: _.noop
+ },
+ log: _.noop
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.INIT_APP_REQUEST,
+ meta: {source: actionSources[actionTypes.INIT_APP_REQUEST]}
+ },
+ {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'test'},
+ meta: {source: actionSources[actionTypes.HIDE_UNAVAILABLE_DEVICES]}
+ },
+ {
+ type: actionTypes.INIT_APP_FAILURE,
+ error: true,
+ payload: new Error(errorText.E_INIT),
+ meta: {source: actionSources[actionTypes.INIT_APP_FAILURE]}
+ }
+ ];
+ asyncActions.__Rewire__('versionInfo', {
+ semver: config.version
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doAppInit(config, servicesToInit));
+ });
+ });
+
+ describe('doLogin [no error]', () => {
+ it('should dispatch LOGIN_REQUEST, LOGIN_SUCCESS actions', (done) => {
+ // NB: this is not what these objects actually look like
+ // actual shape is irrelevant to testing action creators
+ const userObj = {user: {userid: 'abc123'}};
+ const profile = {fullName: 'Jane Doe'};
+ const memberships = [{userid: 'def456'}, {userid: 'ghi789'}];
+ const expectedActions = [
+ {
+ type: actionTypes.LOGIN_REQUEST,
+ meta: {source: actionSources[actionTypes.LOGIN_REQUEST]}
+ },
+ {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: {
+ user: userObj.user,
+ profile, memberships
+ },
+ meta: {
+ source: actionSources[actionTypes.LOGIN_SUCCESS],
+ metric: {eventName: metrics.LOGIN_SUCCESS}
+ }
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ user: {
+ login: (creds, opts, cb) => cb(null, userObj),
+ profile: (cb) => cb(null, profile),
+ getUploadGroups: (cb) => cb(null, memberships)
+ }
+ },
+ log: _.noop
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doLogin(
+ {username: 'jane.doe@me.com', password: 'password'},
+ {remember: false}
+ ));
+ });
+ });
+
+ describe('doLogin [with error]', () => {
+ it('should dispatch LOGIN_REQUEST, LOGIN_FAILURE actions', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.LOGIN_REQUEST,
+ meta: {source: actionSources[actionTypes.LOGIN_REQUEST]}
+ },
+ {
+ type: actionTypes.LOGIN_FAILURE,
+ error: true,
+ payload: new Error(getLoginErrorMessage()),
+ meta: {source: actionSources[actionTypes.LOGIN_FAILURE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ user: {
+ login: (creds, opts, cb) => cb(getLoginErrorMessage()),
+ getUploadGroups: (cb) => cb(null, [])
+ }
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doLogin(
+ {username: 'jane.doe@me.com', password: 'password'},
+ {remember: false}
+ ));
+ });
+ });
+
+ describe('doLogout [no error]', () => {
+ it('should dispatch LOGOUT_REQUEST, LOGOUT_SUCCESS, SET_PAGE actions', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.LOGOUT_REQUEST,
+ meta: {
+ source: actionSources[actionTypes.LOGOUT_REQUEST],
+ metric: {eventName: metrics.LOGOUT_REQUEST}
+ }
+ },
+ {
+ type: actionTypes.LOGOUT_SUCCESS,
+ meta: {source: actionSources[actionTypes.LOGOUT_SUCCESS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.LOGIN},
+ meta: {source: actionSources.USER}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ user: {
+ logout: (cb) => cb(null)
+ }
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doLogout());
+ });
+ });
+
+ describe('doLogout [with error]', () => {
+ it('should dispatch LOGOUT_REQUEST, LOGOUT_FAILURE, SET_PAGE actions', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.LOGOUT_REQUEST,
+ meta: {
+ source: actionSources[actionTypes.LOGOUT_REQUEST],
+ metric: {eventName: metrics.LOGOUT_REQUEST}
+ }
+ },
+ {
+ type: actionTypes.LOGOUT_FAILURE,
+ error: true,
+ payload: new Error(getLogoutErrorMessage()),
+ meta: {source: actionSources[actionTypes.LOGOUT_SUCCESS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.LOGIN},
+ meta: {source: actionSources.USER}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ user: {
+ logout: (cb) => cb('Error :(')
+ }
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doLogout());
+ });
+ });
+
+ describe('doUpload [upload aborted b/c version check failed]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_FAILURE, UPLOAD_ABORTED', (done) => {
+ const requiredVersion = '0.99.0';
+ const currentVersion = '0.50.0';
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: requiredVersion})
+ }
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: new UnsupportedError(currentVersion, requiredVersion),
+ meta: {
+ source: actionSources[actionTypes.VERSION_CHECK_FAILURE],
+ metric: {
+ eventName: metrics.VERSION_CHECK_FAILURE_OUTDATED,
+ properties: { requiredVersion }
+ }
+ }
+ },
+ {
+ type: actionTypes.UPLOAD_ABORTED,
+ error: true,
+ payload: new Error(errorText.E_UPLOAD_IN_PROGRESS),
+ meta: {source: actionSources[actionTypes.UPLOAD_ABORTED]}
+ }
+ ];
+ asyncActions.__Rewire__('versionInfo', {
+ semver: currentVersion
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doUpload());
+ });
+ });
+
+ describe('doUpload [upload aborted b/c another upload already in progress]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_ABORTED', (done) => {
+ const initialState = {
+ working: {uploading: true}
+ };
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_ABORTED,
+ error: true,
+ payload: new Error(errorText.E_UPLOAD_IN_PROGRESS),
+ meta: {source: actionSources[actionTypes.UPLOAD_ABORTED]}
+ }
+ ];
+ asyncActions.__Rewire__('versionInfo', {
+ semver: '0.100.0'
+ });
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload());
+ });
+ });
+
+ describe('doUpload [device, driver error]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, DEVICE_DETECT_REQUEST, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: true},
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const initialState = {
+ devices: {
+ a_pump: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ a_pump: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'a_pump']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_DRIVER'
+ };
+ let err = new Error(`You may need to install the ${targetDevice.name} device driver.`);
+ err.driverLink = urls.DRIVER_DOWNLOAD;
+ err.code = errProps.code;
+ err.utc = errProps.utc;
+ err.version = errProps.version;
+ err.debug = `UTC Time: ${time} | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ device: {
+ detect: (foo, bar, cb) => cb('Error :(')
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed AcmePump',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [device, device detection error (serial)]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, DEVICE_DETECT_REQUEST, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const initialState = {
+ devices: {
+ a_pump: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ a_pump: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'a_pump']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_SERIAL_CONNECTION'
+ };
+ let err = new Error(errorText.E_SERIAL_CONNECTION);
+ err.code = errProps.code;
+ err.utc = errProps.utc;
+ err.version = errProps.version;
+ err.debug = `UTC Time: ${time} | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ device: {
+ detect: (foo, bar, cb) => cb('Error :(')
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed AcmePump',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [device, device detection error (hid)]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, DEVICE_DETECT_REQUEST, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const initialState = {
+ devices: {
+ a_pump: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ a_pump: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'a_pump']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_HID_CONNECTION'
+ };
+ let err = new Error(errorText.E_HID_CONNECTION);
+ err.code = errProps.code;
+ err.utc = errProps.utc;
+ err.version = errProps.version;
+ err.debug = `UTC Time: ${time} | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ device: {
+ detect: (foo, bar, cb) => cb(null, null)
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed AcmePump',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [device, error during upload]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, DEVICE_DETECT_REQUEST, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const initialState = {
+ devices: {
+ a_pump: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ a_pump: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'a_pump']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_DEVICE_UPLOAD'
+ };
+ const basalErr = 'Problem processing basal!';
+ let err = new Error(errorText.E_DEVICE_UPLOAD);
+ err.details = basalErr;
+ err.utc = errProps.utc;
+ err.name = 'Error';
+ err.code = errProps.code;
+ err.version = errProps.version;
+ err.debug = `Details: ${basalErr} | UTC Time: ${time} | Name: Error | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ device: {
+ detect: (foo, bar, cb) => cb(null, {}),
+ upload: (foo, bar, cb) => cb(basalErr)
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed AcmePump',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [no error]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, DEVICE_DETECT_REQUEST, UPLOAD_SUCCESS actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const initialState = {
+ devices: {
+ a_pump: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ a_pump: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'a_pump']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ device: {
+ detect: (foo, bar, cb) => cb(null, {}),
+ upload: (foo, bar, cb) => cb(null, [1,2,3,4,5])
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ },
+ {
+ type: actionTypes.UPLOAD_SUCCESS,
+ payload: { userId, deviceKey, utc: time, data: [1,2,3,4,5] },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_SUCCESS],
+ metric: {
+ eventName: 'Upload Successful AcmePump',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ started: time,
+ finished: time,
+ processed: 5
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [CareLink fetch error]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, CARELINK_FETCH_REQUEST, CARELINK_FETCH_FAILURE, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'carelink';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'CareLink',
+ showDriverLink: {mac: false},
+ source: {type: 'carelink'}
+ };
+ const initialState = {
+ devices: {
+ carelink: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ carelink: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'carelink']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_FETCH_CARELINK'
+ };
+ let err = new Error(errorText.E_FETCH_CARELINK);
+ err.details = 'Error!';
+ err.utc = errProps.utc;
+ err.code = errProps.code;
+ err.version = errProps.version;
+ err.debug = `Details: Error! | UTC Time: ${time} | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ fetchCarelinkData: (foo, cb) => cb(new Error('Error!')),
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted CareLink',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_FAILURE,
+ error: true,
+ payload: new Error(errorText.E_FETCH_CARELINK),
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_FAILURE],
+ metric: {eventName: metrics.CARELINK_FETCH_FAILURE}
+ }
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed CareLink',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [CareLink fetch, incorrect creds]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, CARELINK_FETCH_REQUEST, CARELINK_FETCH_FAILURE, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'carelink';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'CareLink',
+ showDriverLink: {mac: false},
+ source: {type: 'carelink'}
+ };
+ const initialState = {
+ devices: {
+ carelink: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ carelink: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'carelink']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_CARELINK_CREDS'
+ };
+ let err = new Error(errorText.E_CARELINK_CREDS);
+ err.utc = errProps.utc;
+ err.code = errProps.code;
+ err.version = errProps.version;
+ err.debug = `UTC Time: ${time} | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ fetchCarelinkData: (foo, cb) => cb(null, '302 Moved Temporarily'),
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted CareLink',
+ properties: {type: targetDevice.source.type, source: targetDevice.source.driverId}
+ }
+ }
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_FAILURE,
+ error: true,
+ payload: new Error(errorText.E_CARELINK_CREDS),
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_FAILURE],
+ metric: {eventName: metrics.CARELINK_FETCH_FAILURE}
+ }
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed CareLink',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [CareLink, error in processing & uploading]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, CARELINK_FETCH_REQUEST, CARELINK_FETCH_SUCCESS, UPLOAD_FAILURE actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'carelink';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'carelink'}
+ };
+ const initialState = {
+ devices: {
+ carelink: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ carelink: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'carelink']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ const errProps = {
+ utc: time,
+ version: initialState.version,
+ code: 'E_CARELINK_UPLOAD'
+ };
+ const basalErr = 'Problem processing basal!';
+ let err = new Error(errorText.E_CARELINK_UPLOAD);
+ err.details = basalErr;
+ err.utc = errProps.utc;
+ err.name = 'Error';
+ err.code = errProps.code;
+ err.version = errProps.version;
+ err.debug = `Details: ${basalErr} | UTC Time: ${time} | Name: Error | Code: ${errProps.code} | Version: ${errProps.version}`;
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ fetchCarelinkData: (foo, cb) => cb(null, '1,2,3,4,5'),
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ carelink: {
+ upload: (foo, bar, cb) => cb(new Error(basalErr))
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted CareLink',
+ properties: {type: targetDevice.source.type, source: undefined}
+ }
+ }
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_SUCCESS,
+ payload: { userId, deviceKey },
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_SUCCESS],
+ metric: {eventName: metrics.CARELINK_FETCH_SUCCESS}
+ }
+ },
+ {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: 'Upload Failed CareLink',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ error: err
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('doUpload [CareLink, no error]', () => {
+ it('should dispatch VERSION_CHECK_REQUEST, VERSION_CHECK_SUCCESS, UPLOAD_REQUEST, CARELINK_FETCH_REQUEST, CARELINK_FETCH_SUCCESS, UPLOAD_SUCCESS actions', (done) => {
+ const userId = 'a1b2c3', deviceKey = 'carelink';
+ const time = '2016-01-01T12:05:00.123Z';
+ const targetDevice = {
+ key: deviceKey,
+ name: 'Acme Insulin Pump',
+ showDriverLink: {mac: false},
+ source: {type: 'carelink'}
+ };
+ const initialState = {
+ devices: {
+ carelink: targetDevice
+ },
+ os: 'mac',
+ uploadsByUser: {
+ [userId]: {
+ a_cgm: {},
+ carelink: {history: [{start: time}]}
+ }
+ },
+ targetDevices: {
+ [userId]: ['a_cgm', 'carelink']
+ },
+ targetTimezones: {
+ [userId]: 'US/Mountain'
+ },
+ uploadTargetDevice: deviceKey,
+ uploadTargetUser: userId,
+ version: '0.100.0',
+ working: {uploading: false}
+ };
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ fetchCarelinkData: (foo, cb) => cb(null, '1,2,3,4,5'),
+ getVersions: (cb) => cb(null, {uploaderMinimum: '0.99.0'})
+ }
+ },
+ carelink: {
+ upload: (foo, bar, cb) => cb(null, [1,2,3,4])
+ }
+ });
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ },
+ {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted CareLink',
+ properties: {type: targetDevice.source.type, source: undefined}
+ }
+ }
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ },
+ {
+ type: actionTypes.CARELINK_FETCH_SUCCESS,
+ payload: { userId, deviceKey },
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_SUCCESS],
+ metric: {eventName: metrics.CARELINK_FETCH_SUCCESS}
+ }
+ },
+ {
+ type: actionTypes.UPLOAD_SUCCESS,
+ payload: { userId, deviceKey, utc: time, data: [1,2,3,4] },
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_SUCCESS],
+ metric: {
+ eventName: 'Upload Successful CareLink',
+ properties: {
+ type: targetDevice.source.type,
+ source: targetDevice.source.driverId,
+ started: time,
+ finished: time,
+ processed: 4
+ }
+ }
+ }
+ }
+ ];
+ const store = mockStore(initialState, expectedActions, done);
+ store.dispatch(asyncActions.doUpload(deviceKey, {}, time));
+ });
+ });
+
+ describe('readFile', () => {
+ describe('wrong file extension chosen', () => {
+ it('should dispatch CHOOSING_FILE, READ_FILE_ABORTED actions', (done) => {
+ const userId = 'abc123', deviceKey = 'a_pump', ext = '.abc', version = '0.100.0';
+ let err = new Error(errorText.E_FILE_EXT + ext);
+ err.code = 'E_FILE_EXT';
+ err.version = version;
+ err.debug = `Code: ${err.code} | Version: ${version}`;
+ const expectedActions = [
+ {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CHOOSING_FILE]}
+ },
+ {
+ type: actionTypes.READ_FILE_ABORTED,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.READ_FILE_ABORTED]}
+ }
+ ];
+ const state = {
+ version: version
+ };
+ const store = mockStore(state, expectedActions, done);
+ store.dispatch(asyncActions.readFile(userId, deviceKey, {name: 'data.csv'}, ext));
+ });
+ });
+ });
+
+ describe('doVersionCheck', () => {
+ describe('API error when attempting to get versions info from jellyfish', () => {
+ it('should dispatch VERSION_CHECK_REQUEST and VERSION_CHECK_FAILURE', (done) => {
+ const err = new Error('API error!');
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_FAILURE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => { cb(err); }
+ }
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doVersionCheck());
+ });
+ });
+
+ describe('missing or invalid semver in response from jellyfish', () => {
+ it('should dispatch VERSION_CHECK_REQUEST and VERSION_CHECK_FAILURE', (done) => {
+ const err = new Error('Invalid semver [foo.bar]');
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_FAILURE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => { cb(err); }
+ }
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doVersionCheck());
+ });
+ });
+
+ describe('uploader\'s version is below the required minimum', () => {
+ it('should dispatch VERSION_CHECK_REQUEST and VERSION_CHECK_FAILURE', (done) => {
+ const currentVersion = '0.99.0', requiredVersion = '0.100.0';
+ const err = new UnsupportedError(currentVersion, requiredVersion);
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.VERSION_CHECK_FAILURE],
+ metric: {
+ eventName: metrics.VERSION_CHECK_FAILURE_OUTDATED,
+ properties: { requiredVersion }
+ }
+ }
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => { cb(null, {uploaderMinimum: requiredVersion}); }
+ }
+ }
+ });
+ asyncActions.__Rewire__('versionInfo', {
+ semver: currentVersion
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doVersionCheck());
+ });
+ });
+
+ describe('uploader\'s version meets the minimum', () => {
+ it('should dispatch VERSION_CHECK_REQUEST and VERSION_CHECK_SUCCESS', (done) => {
+ const currentVersion = '0.100.0', requiredVersion = '0.100.0';
+ const expectedActions = [
+ {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ },
+ {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ upload: {
+ getVersions: (cb) => { cb(null, {uploaderMinimum: requiredVersion}); }
+ }
+ }
+ });
+ asyncActions.__Rewire__('versionInfo', {
+ semver: currentVersion
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.doVersionCheck());
+ });
+ });
+ });
+
+ describe('putTargetsInStorage', () => {
+ describe('no targets in local storage', () => {
+ it('should dispatch RETRIEVING_USERS_TARGETS, STORING_USERS_TARGETS, SET_PAGE (redirect to main page)', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.STORING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.STORING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.MAIN},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ localStore: {
+ getItem: () => null,
+ setItem: () => null
+ }
+ });
+ const state = {
+ targetDevices: {
+ abc123: ['a_pump', 'a_bg_meter']
+ },
+ targetTimezones: {
+ abc123: 'Europe/Budapest'
+ },
+ uploadTargetUser: 'abc123'
+ };
+ const store = mockStore(state, expectedActions, done);
+ store.dispatch(asyncActions.putTargetsInStorage());
+ });
+ });
+
+ describe('existing targets in local storage', () => {
+ it('should dispatch RETRIEVING_USERS_TARGETS, STORING_USERS_TARGETS, SET_PAGE (redirect to main page)', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.STORING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.STORING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.MAIN},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ localStore: {
+ getItem: () => null,
+ setItem: () => { return {
+ abc123: [
+ {key: 'a_pump', timezone: 'US/Central'}
+ ],
+ def456: [
+ {key: 'a_pump', timezone: 'US/Eastern'},
+ {key: 'a_cgm', timezone: 'US/Eastern'}
+ ]
+ }; }
+ }
+ });
+ const state = {
+ targetDevices: {
+ abc123: ['a_pump', 'a_bg_meter']
+ },
+ targetTimezones: {
+ abc123: 'Europe/Budapest'
+ },
+ uploadTargetUser: 'abc123'
+ };
+ const store = mockStore(state, expectedActions, done);
+ store.dispatch(asyncActions.putTargetsInStorage());
+ });
+ });
+ });
+
+ describe('retrieveTargetsFromStorage', () => {
+ const url = 'http://acme-blip.com/patients/abc123/data';
+ const blipUrlMaker = (path) => { return 'http://acme-blip.com' + path; };
+ describe('no targets retrieved from local storage', () => {
+ it('should dispatch SET_PAGE (redirect to settings page)', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.SETTINGS},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ localStore: {
+ getItem: () => null
+ }
+ });
+ const store = mockStore({}, expectedActions, done);
+ store.dispatch(asyncActions.retrieveTargetsFromStorage());
+ });
+ });
+
+ describe('targets retrieved, but no user targeted for upload by default', () => {
+ 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: [
+ {key: 'dexcom', timezone: 'US/Mountain'},
+ {key: 'omnipod', timezone: 'US/Mountain'}
+ ]
+ };
+ const devicesByUser = {
+ abc123: ['carelink'],
+ def456: ['dexcom', 'omnipod']
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_UPLOADS,
+ payload: { devicesByUser },
+ 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({
+ allUsers: {
+ ghi789: {},
+ abc123: {},
+ def456: {},
+ },
+ loggedInUser: 'ghi789',
+ 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_UPLOADS, SET_USERS_TARGETS, 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 devicesByUser = {
+ abc123: ['carelink'],
+ def456: ['dexcom', 'omnipod']
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_UPLOADS,
+ payload: { devicesByUser },
+ 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', {
+ api: {
+ makeBlipUrl: blipUrlMaker
+ },
+ localStore: {
+ getItem: () => targets
+ }
+ });
+ const store = mockStore({
+ allUsers: {
+ ghi789: {},
+ abc123: {},
+ def456: {},
+ },
+ devices: {
+ carelink: {},
+ dexcom: {},
+ omnipod: {}
+ },
+ loggedInUser: 'ghi789',
+ 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_UPLOADS, SET_USERS_TARGETS, 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 devicesByUser = {
+ abc123: ['carelink'],
+ def456: ['dexcom', 'omnipod']
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_UPLOADS,
+ payload: { devicesByUser },
+ 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', {
+ api: {
+ makeBlipUrl: blipUrlMaker
+ },
+ localStore: {
+ getItem: () => targets
+ }
+ });
+ const store = mockStore({
+ allUsers: {
+ ghi789: {},
+ abc123: {},
+ def456: {},
+ },
+ devices: {
+ dexcom: {},
+ omnipod: {}
+ },
+ loggedInUser: 'ghi789',
+ 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_UPLOADS, SET_USERS_TARGETS, 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 devicesByUser = {
+ abc123: ['carelink'],
+ def456: ['dexcom', 'omnipod']
+ };
+ const expectedActions = [
+ {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ },
+ {
+ type: actionTypes.SET_UPLOADS,
+ payload: { devicesByUser },
+ 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.MAIN},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', {
+ api: {
+ makeBlipUrl: blipUrlMaker
+ },
+ localStore: {
+ getItem: () => targets
+ }
+ });
+ const store = mockStore({
+ allUsers: {
+ ghi789: {},
+ abc123: {},
+ def456: {},
+ },
+ devices: {
+ carelink: {},
+ dexcom: {},
+ omnipod: {}
+ },
+ loggedInUser: 'ghi789',
+ uploadTargetUser: 'abc123'
+ }, expectedActions, done);
+ store.dispatch(asyncActions.retrieveTargetsFromStorage());
+ });
+ });
+ });
+
+ describe('setUploadTargetUserAndMaybeRedirect', () => {
+ const userId = 'abc123', url = 'http://acme-blip.com/patients/abc123/data';
+ const apiRewire = {
+ api: {
+ makeBlipUrl: (path) => { return 'http://acme-blip.com' + path; }
+ }
+ };
+ describe('new target user has selected devices and timezone', () => {
+ it('should dispatch just SET_UPLOAD_TARGET_USER and SET_BLIP_VIEW_DATA_URL', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: { userId },
+ meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]}
+ },
+ {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ }
+ ];
+ asyncActions.__Rewire__('services', apiRewire);
+ 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 SET_UPLOAD_TARGET_USER, SET_BLIP_VIEW_DATA_URL, and SET_PAGE (redirect to settings)', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: { userId },
+ meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]}
+ },
+ {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.SETTINGS},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', apiRewire);
+ 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 SET_UPLOAD_TARGET_USER, SET_BLIP_VIEW_DATA_URL, and SET_PAGE (redirect to settings)', (done) => {
+ const expectedActions = [
+ {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: { userId },
+ meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]}
+ },
+ {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ },
+ {
+ type: actionTypes.SET_PAGE,
+ payload: {page: pages.SETTINGS},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ }
+ ];
+ asyncActions.__Rewire__('services', apiRewire);
+ 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
new file mode 100644
index 0000000000..44b9f4bbe2
--- /dev/null
+++ b/test/browser/redux/actions/sync.test.js
@@ -0,0 +1,983 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2014, 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/*eslint-env mocha*/
+
+import _ from 'lodash';
+import { isFSA } from 'flux-standard-action';
+
+import * as actionSources from '../../../../lib/redux/constants/actionSources';
+import * as actionTypes from '../../../../lib/redux/constants/actionTypes';
+import * as metrics from '../../../../lib/redux/constants/metrics';
+import { steps } from '../../../../lib/redux/constants/otherConstants';
+
+import * as syncActions from '../../../../lib/redux/actions/sync';
+import { errorText, UnsupportedError } from '../../../../lib/redux/utils/errors';
+
+describe('Synchronous Actions', () => {
+ describe('addTargetDevice', () => {
+ const DEVICE = 'a_pump', ID = 'a1b2c3';
+ it('should be an FSA', () => {
+ let action = syncActions.addTargetDevice(ID, DEVICE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to add a device to a user\'s target devices', () => {
+ const expectedAction = {
+ type: actionTypes.ADD_TARGET_DEVICE,
+ payload: {userId: ID, deviceKey: DEVICE},
+ meta: {source: actionSources[actionTypes.ADD_TARGET_DEVICE]}
+ };
+ expect(syncActions.addTargetDevice(ID, DEVICE)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('clickGoToBlip', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.clickGoToBlip();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report a user\'s clicking of the \'Go to Blip\' button', () => {
+ const expectedAction = {
+ type: actionTypes.CLICK_GO_TO_BLIP,
+ meta: {
+ source: actionSources[actionTypes.CLICK_GO_TO_BLIP],
+ metric: {eventName: metrics.CLICK_GO_TO_BLIP}
+ }
+ };
+
+ expect(syncActions.clickGoToBlip()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('hideUnavailableDevices', () => {
+ const OS = 'test';
+ it('should be an FSA', () => {
+ let action = syncActions.hideUnavailableDevices(OS);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to hide devices unavailable on given operating system', () => {
+ const expectedAction = {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: OS},
+ meta: {source: actionSources[actionTypes.HIDE_UNAVAILABLE_DEVICES]}
+ };
+ expect(syncActions.hideUnavailableDevices(OS)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('removeTargetDevice', () => {
+ const DEVICE = 'a_pump', ID = 'a1b2c3';
+ it('should be an FSA', () => {
+ let action = syncActions.removeTargetDevice(ID, DEVICE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to remove a device from a user\'s target devices', () => {
+ const expectedAction = {
+ type: actionTypes.REMOVE_TARGET_DEVICE,
+ payload: {userId: ID, deviceKey: DEVICE},
+ meta: {source: actionSources[actionTypes.REMOVE_TARGET_DEVICE]}
+ };
+ expect(syncActions.removeTargetDevice(ID, DEVICE)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('resetUpload', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ it('should be an FSA', () => {
+ let action = syncActions.resetUpload(userId, deviceKey);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to reset an upload after failure or success', () => {
+ const expectedAction = {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.RESET_UPLOAD]}
+ };
+ expect(syncActions.resetUpload(userId, deviceKey)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setBlipViewDataUrl', () => {
+ const url = 'http://acme-blip.com/patients/a1b2c3/data';
+ it('should be an FSA', () => {
+ let action = syncActions.setBlipViewDataUrl(url);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action set the url for viewing data in blip (wrt current uploadTargetUser)', () => {
+ const expectedAction = {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: { url },
+ meta: {source: actionSources[actionTypes.SET_BLIP_VIEW_DATA_URL]}
+ };
+
+ expect(syncActions.setBlipViewDataUrl(url)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setForgotPasswordUrl', () => {
+ const URL = 'http://www.acme.com/forgot-password';
+ it('should be an FSA', () => {
+ let action = syncActions.setForgotPasswordUrl(URL);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the forgot password url', () => {
+ const expectedAction = {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: {url: URL},
+ meta: {source: actionSources[actionTypes.SET_FORGOT_PASSWORD_URL]}
+ };
+ expect(syncActions.setForgotPasswordUrl(URL)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setPage', () => {
+ const PAGE = 'FOO';
+ it('should be an FSA', () => {
+ let action = syncActions.setPage(PAGE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the page', () => {
+ const expectedAction = {
+ type: actionTypes.SET_PAGE,
+ payload: {page: PAGE},
+ meta: {source: actionSources[actionTypes.SET_PAGE]}
+ };
+ expect(syncActions.setPage(PAGE)).to.deep.equal(expectedAction);
+ });
+
+ it('should accept a second parameter to override the default action source', () => {
+ const expectedAction = {
+ type: actionTypes.SET_PAGE,
+ payload: {page: PAGE},
+ meta: {source: actionSources.USER}
+ };
+ expect(syncActions.setPage(PAGE, actionSources.USER)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setSignUpUrl', () => {
+ const URL = 'http://www.acme.com/sign-up';
+ it('should be an FSA', () => {
+ let action = syncActions.setSignUpUrl(URL);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the sign-up url', () => {
+ const expectedAction = {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: {url: URL},
+ meta: {source: actionSources[actionTypes.SET_SIGNUP_URL]}
+ };
+ expect(syncActions.setSignUpUrl(URL)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setTargetTimezone', () => {
+ const TIMEZONE = 'Europe/Budapest', ID = 'a1b2c3';
+ it('should be an FSA', () => {
+ let action = syncActions.setTargetTimezone(ID, TIMEZONE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the target timezone for a user', () => {
+ const expectedAction = {
+ type: actionTypes.SET_TARGET_TIMEZONE,
+ payload: {userId: ID, timezoneName: TIMEZONE},
+ meta: {source: actionSources[actionTypes.SET_TARGET_TIMEZONE]}
+ };
+ expect(syncActions.setTargetTimezone(ID, TIMEZONE)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setUploads', () => {
+ const devicesByUser = {
+ a1b2c3: {a_pump: {}, a_cgm: {}},
+ d4e5f6: {another_pump: {}}
+ };
+ it('should be an FSA', () => {
+ let action = syncActions.setUploads(devicesByUser);
+
+ 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: { devicesByUser },
+ meta: {source: actionSources[actionTypes.SET_UPLOADS]}
+ };
+ expect(syncActions.setUploads(devicesByUser)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setUploadTargetUser', () => {
+ const ID = 'a1b2c3';
+ it('should be an FSA', () => {
+ let action = syncActions.setUploadTargetUser(ID);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the target user for data upload', () => {
+ const expectedAction = {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: {userId: ID},
+ meta: {source: actionSources[actionTypes.SET_UPLOAD_TARGET_USER]}
+ };
+ expect(syncActions.setUploadTargetUser(ID)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('toggleDropdown', () => {
+ const DROPDOWN_PREVIOUS_STATE = true;
+ it('should be an FSA', () => {
+ let action = syncActions.toggleDropdown(DROPDOWN_PREVIOUS_STATE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to toggle the dropdown menu', () => {
+ const expectedAction = {
+ type: actionTypes.TOGGLE_DROPDOWN,
+ payload: {isVisible: false},
+ meta: {source: actionSources[actionTypes.TOGGLE_DROPDOWN]}
+ };
+ expect(syncActions.toggleDropdown(DROPDOWN_PREVIOUS_STATE)).to.deep.equal(expectedAction);
+ });
+
+ it('should accept a second parameter to override the default action source', () => {
+ const expectedAction = {
+ type: actionTypes.TOGGLE_DROPDOWN,
+ payload: {isVisible: false},
+ meta: {source: actionSources.UNDER_THE_HOOD}
+ };
+ expect(syncActions.toggleDropdown(DROPDOWN_PREVIOUS_STATE, actionSources.UNDER_THE_HOOD)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('toggleErrorDetails', () => {
+ const DETAILS_PREVIOUS_STATE = true;
+ const userId = 'a1b2c3', deviceKey = 'a_cgm';
+ it('should be an FSA', () => {
+ let action = syncActions.toggleErrorDetails(userId, deviceKey, DETAILS_PREVIOUS_STATE);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to toggle error details for an upload', () => {
+ const expectedAction = {
+ type: actionTypes.TOGGLE_ERROR_DETAILS,
+ payload: {isVisible: false, userId, deviceKey },
+ meta: {source: actionSources[actionTypes.TOGGLE_ERROR_DETAILS]}
+ };
+ expect(syncActions.toggleErrorDetails(userId, deviceKey, DETAILS_PREVIOUS_STATE)).to.deep.equal(expectedAction);
+ });
+
+ it('should toggle on error details if previous state is undefined', () => {
+ const expectedAction = {
+ type: actionTypes.TOGGLE_ERROR_DETAILS,
+ payload: {isVisible: true, userId, deviceKey },
+ meta: {source: actionSources[actionTypes.TOGGLE_ERROR_DETAILS]}
+ };
+ expect(syncActions.toggleErrorDetails(userId, deviceKey, undefined)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('for doAppInit', () => {
+ describe('initRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.initRequest();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the start of app initialization', () => {
+ const expectedAction = {
+ type: actionTypes.INIT_APP_REQUEST,
+ meta: {source: actionSources[actionTypes.INIT_APP_REQUEST]}
+ };
+ expect(syncActions.initRequest()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('initSuccess', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.initSuccess();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the successful completion of app initialization', () => {
+ const expectedAction = {
+ type: actionTypes.INIT_APP_SUCCESS,
+ meta: {source: actionSources[actionTypes.INIT_APP_SUCCESS]}
+ };
+ expect(syncActions.initSuccess()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('initFailure', () => {
+ const err = new Error();
+ it('should be an FSA', () => {
+ let action = syncActions.initFailure(err);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record early exit from app initialization due to error', () => {
+ const expectedAction = {
+ type: actionTypes.INIT_APP_FAILURE,
+ error: true,
+ payload: new Error(errorText.E_INIT),
+ meta: {source: actionSources[actionTypes.INIT_APP_FAILURE]}
+ };
+ expect(syncActions.initFailure(err)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setUserInfoFromToken', () => {
+ // NB: this is not what these objects actually look like
+ // actual shape is irrelevant to testing action creators
+ const user = {userid: 'abc123'};
+ const profile = {fullName: 'Jane Doe'};
+ const memberships = [{userid: 'def456'}, {userid: 'ghi789'}];
+ it('should be an FSA', () => {
+ let action = syncActions.setUserInfoFromToken({ user, profile, memberships });
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the logged-in user (plus user\'s profile, careteam memberships)', () => {
+ const expectedAction = {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships },
+ meta: {source: actionSources[actionTypes.SET_USER_INFO_FROM_TOKEN]}
+ };
+ expect(syncActions.setUserInfoFromToken({ user, profile, memberships })).to.deep.equal(expectedAction);
+ });
+ });
+ });
+
+ describe('for doLogin', () => {
+ describe('loginRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.loginRequest();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the start of user login', () => {
+ const expectedAction = {
+ type: actionTypes.LOGIN_REQUEST,
+ meta: {source: actionSources[actionTypes.LOGIN_REQUEST]}
+ };
+ expect(syncActions.loginRequest()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('loginSuccess', () => {
+ // NB: this is not what these objects actually look like
+ // actual shape is irrelevant to testing action creators
+ const user = {userid: 'abc123'};
+ const profile = {fullName: 'Jane Doe'};
+ const memberships = [{userid: 'def456'}, {userid: 'ghi789'}];
+ it('should be an FSA', () => {
+ expect(isFSA(syncActions.loginSuccess({ user, profile, memberships }))).to.be.true;
+ });
+
+ it('should create an action to set the logged-in user (plus user\'s profile, careteam memberships)', () => {
+ const expectedAction = {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships },
+ meta: {
+ source: actionSources[actionTypes.LOGIN_SUCCESS],
+ metric: {eventName: metrics.LOGIN_SUCCESS}
+ }
+ };
+ expect(syncActions.loginSuccess({ user, profile, memberships })).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('loginFailure', () => {
+ const err = 'Login error!';
+ it('should be an FSA', () => {
+ let action = syncActions.loginFailure(err);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report a login error', () => {
+ syncActions.__Rewire__('getLoginErrorMessage', () => err);
+ const expectedAction = {
+ type: actionTypes.LOGIN_FAILURE,
+ error: true,
+ payload: new Error(err),
+ meta: {source: actionSources[actionTypes.LOGIN_FAILURE]}
+ };
+ expect(syncActions.loginFailure(err)).to.deep.equal(expectedAction);
+ syncActions.__ResetDependency__('getLoginErrorMessage');
+ });
+ });
+ });
+
+ describe('for doLogout', () => {
+ describe('logoutRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.logoutRequest();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to request logout and clear logged-in user related state', () => {
+ const expectedAction = {
+ type: actionTypes.LOGOUT_REQUEST,
+ meta: {
+ source: actionSources[actionTypes.LOGOUT_REQUEST],
+ metric: {eventName: metrics.LOGOUT_REQUEST}
+ }
+ };
+ expect(syncActions.logoutRequest()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('logoutSuccess', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.logoutSuccess();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to announce the success of logout', () => {
+ const expectedAction = {
+ type: actionTypes.LOGOUT_SUCCESS,
+ meta: {source: actionSources[actionTypes.LOGOUT_SUCCESS]}
+ };
+ expect(syncActions.logoutSuccess()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('logoutFailure', () => {
+ const err = 'Logout error!';
+ it('should be an FSA', () => {
+ let action = syncActions.logoutFailure(err);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report a logout error', () => {
+ syncActions.__Rewire__('getLogoutErrorMessage', () => err);
+ const expectedAction = {
+ type: actionTypes.LOGOUT_FAILURE,
+ error: true,
+ payload: new Error(err),
+ meta: {source: actionSources[actionTypes.LOGOUT_FAILURE]}
+ };
+ expect(syncActions.logoutFailure(err)).to.deep.equal(expectedAction);
+ syncActions.__ResetDependency__('getLoginErrorMessage');
+ });
+ });
+ });
+
+ describe('for doCareLinkUpload', () => {
+ const userId = 'a1b2c3', deviceKey = 'carelink';
+ describe('fetchCareLinkRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.fetchCareLinkRequest(userId, deviceKey);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record an attempt to fetch data from CareLink', () => {
+ const expectedAction = {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CARELINK_FETCH_REQUEST]}
+ };
+
+ expect(syncActions.fetchCareLinkRequest(userId, deviceKey)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('fetchCareLinkSuccess', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.fetchCareLinkSuccess(userId, deviceKey);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record a successful fetch from CareLink', () => {
+ const expectedAction = {
+ type: actionTypes.CARELINK_FETCH_SUCCESS,
+ payload: { userId, deviceKey },
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_SUCCESS],
+ metric: {eventName: metrics.CARELINK_FETCH_SUCCESS}
+ }
+ };
+
+ expect(syncActions.fetchCareLinkSuccess(userId, deviceKey)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('fetchCareLinkFailure', () => {
+ const err = new Error('Error :(');
+ it('should be an FSA', () => {
+ let action = syncActions.fetchCareLinkFailure('Error :(');
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the failure of an attempt to fetch data from CareLink', () => {
+ const expectedAction = {
+ type: actionTypes.CARELINK_FETCH_FAILURE,
+ error: true,
+ payload: err,
+ meta: {
+ source: actionSources[actionTypes.CARELINK_FETCH_FAILURE],
+ metric: {eventName: metrics.CARELINK_FETCH_FAILURE}
+ }
+ };
+ expect(syncActions.fetchCareLinkFailure('Error :(')).to.deep.equal(expectedAction);
+ });
+
+ });
+ });
+
+ describe('for doUpload', () => {
+ describe('uploadAborted', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.uploadAborted();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action reporting an aborted upload (b/c another upload in progress)', () => {
+ const expectedAction = {
+ type: actionTypes.UPLOAD_ABORTED,
+ error: true,
+ payload: new Error(errorText.E_UPLOAD_IN_PROGRESS),
+ meta: {source: actionSources[actionTypes.UPLOAD_ABORTED]}
+ };
+
+ expect(syncActions.uploadAborted()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('uploadRequest', () => {
+ const userId = 'a1b2c3';
+ const device = {
+ key: 'a_pump',
+ name: 'Acme Pump',
+ showDriverLink: {mac: true, win: true},
+ source: {type: 'device', driverId: 'AcmePump'},
+ enabled: {mac: true, win: true}
+ };
+ it('should be an FSA', () => {
+ let action = syncActions.uploadRequest(userId, device);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the start of an upload', () => {
+ const time = '2016-01-01T12:05:00.123Z';
+ const expectedAction = {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey: device.key, utc: time},
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_REQUEST],
+ metric: {
+ eventName: 'Upload Attempted AcmePump',
+ properties: {type: device.source.type, source: device.source.driverId}
+ }
+ }
+ };
+
+ expect(syncActions.uploadRequest(userId, device, time)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('uploadProgress', () => {
+ const step = 'READ', percentage = 50;
+ it('should be an FSA', () => {
+ let action = syncActions.uploadProgress(step, percentage);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to update the step and percentage complete for the upload in progress', () => {
+ const expectedAction = {
+ type: actionTypes.UPLOAD_PROGRESS,
+ payload: { step, percentage },
+ meta: {source: actionSources[actionTypes.UPLOAD_PROGRESS]}
+ };
+
+ expect(syncActions.uploadProgress(step, percentage)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('uploadSuccess', () => {
+ const time = '2016-01-01T12:05:00.123Z';
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const device = {
+ key: deviceKey,
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ const upload = {
+ history: [{start: time}]
+ };
+ const data = [1,2,3,4,5];
+ it('should be an FSA', () => {
+ let action = syncActions.uploadSuccess(userId, device, upload, data);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record a successful upload', () => {
+ const expectedAction = {
+ type: actionTypes.UPLOAD_SUCCESS,
+ payload: { userId, deviceKey: device.key, data, utc: time},
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_SUCCESS],
+ metric: {
+ eventName: `${metrics.UPLOAD_SUCCESS} ${device.source.driverId}`,
+ properties: {
+ type: device.source.type,
+ source: device.source.driverId,
+ started: time,
+ finished: time,
+ processed: data.length
+ }
+ }
+ }
+ };
+ expect(syncActions.uploadSuccess(userId, device, upload, data, time)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('uploadFailure', () => {
+ const origError = new Error('I\'m an upload error!');
+ const errProps = {
+ utc: '2016-01-01T12:05:00.123Z',
+ code: 'RED'
+ };
+ let resError = new Error('I\'m an upload error!');
+ resError.code = errProps.code;
+ resError.utc = errProps.utc;
+ resError.debug = `UTC Time: ${errProps.utc} | Code: ${errProps.code}`;
+ const device = {
+ source: {type: 'device', driverId: 'AcmePump'}
+ };
+ it('should be an FSA', () => {
+ let action = syncActions.uploadFailure(origError, errProps, device);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report an upload failure', () => {
+ const expectedAction = {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: resError,
+ meta: {
+ source: actionSources[actionTypes.UPLOAD_FAILURE],
+ metric: {
+ eventName: `${metrics.UPLOAD_FAILURE} ${device.source.driverId}`,
+ properties: {
+ type: device.source.type,
+ source: device.source.driverId,
+ error: resError
+ }
+ }
+ }
+ };
+ expect(syncActions.uploadFailure(origError, errProps, device)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('deviceDetectRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.deviceDetectRequest();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action record an attempt to detect a device', () => {
+ const expectedAction = {
+ type: actionTypes.DEVICE_DETECT_REQUEST,
+ meta: {source: actionSources[actionTypes.DEVICE_DETECT_REQUEST]}
+ };
+
+ expect(syncActions.deviceDetectRequest()).to.deep.equal(expectedAction);
+ });
+ });
+ });
+
+ describe('for readFile', () => {
+ describe('choosingFile', () => {
+ const userId = 'abc123', deviceKey = 'a_pump';
+ it('should be an FSA', () => {
+ let action = syncActions.choosingFile(userId, deviceKey);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record file selection for a block-mode device', () => {
+ const expectedAction = {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { userId, deviceKey },
+ meta: {source: actionSources[actionTypes.CHOOSING_FILE]}
+ };
+
+ expect(syncActions.choosingFile(userId, deviceKey)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('readFileAborted', () => {
+ let err = new Error('Wrong file extension!');
+ it('should be an FSA', () => {
+ let action = syncActions.readFileAborted(err);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report user error in choosing a file with the wrong extension', () => {
+ const expectedAction = {
+ type: actionTypes.READ_FILE_ABORTED,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.READ_FILE_ABORTED]}
+ };
+
+ expect(syncActions.readFileAborted(err)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('readFileRequest', () => {
+ const userId = 'abc123', deviceKey = 'a_pump';
+ const filename = 'my-data.ext';
+ it('should be an FSA', () => {
+ let action = syncActions.readFileRequest(userId, deviceKey, filename);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record request to read a chosen file', () => {
+ const expectedAction = {
+ type: actionTypes.READ_FILE_REQUEST,
+ payload: { userId, deviceKey, filename },
+ meta: {source: actionSources[actionTypes.READ_FILE_REQUEST]}
+ };
+
+ expect(syncActions.readFileRequest(userId, deviceKey, filename)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('readFileSuccess', () => {
+ const userId = 'abc123', deviceKey = 'a_pump';
+ const filedata = [1,2,3,4,5];
+ it('should be an FSA', () => {
+ let action = syncActions.readFileSuccess(userId, deviceKey, filedata);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record data of successfully read file', () => {
+ const expectedAction = {
+ type: actionTypes.READ_FILE_SUCCESS,
+ payload: { userId, deviceKey, filedata },
+ meta: {source: actionSources[actionTypes.READ_FILE_SUCCESS]}
+ };
+
+ expect(syncActions.readFileSuccess(userId, deviceKey, filedata)).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('readFileFailure', () => {
+ let err = new Error('Error reading file!');
+ it('should be an FSA', () => {
+ let action = syncActions.readFileFailure(err);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to report error reading chosen file', () => {
+ const expectedAction = {
+ type: actionTypes.READ_FILE_FAILURE,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.READ_FILE_FAILURE]}
+ };
+
+ expect(syncActions.readFileFailure(err)).to.deep.equal(expectedAction);
+ });
+ });
+ });
+
+ describe('for doVersionCheck', () => {
+ describe('versionCheckRequest', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.versionCheckRequest();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record the start of the uploader supported version check', () => {
+ const expectedAction = {
+ type: actionTypes.VERSION_CHECK_REQUEST,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_REQUEST]}
+ };
+
+ expect(syncActions.versionCheckRequest()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('versionCheckSuccess', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.versionCheckSuccess();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to mark the current uploader\'s version as supported', () => {
+ const expectedAction = {
+ type: actionTypes.VERSION_CHECK_SUCCESS,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_SUCCESS]}
+ };
+
+ expect(syncActions.versionCheckSuccess()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('versionCheckFailure', () => {
+ const currentVersion = '0.99.0', requiredVersion = '0.100.0';
+ it('should be an FSA [API response err]', () => {
+ let action = syncActions.versionCheckFailure(new Error('API error!'));
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should be an FSA [out-of-date version]', () => {
+ let action = syncActions.versionCheckFailure(null, currentVersion, requiredVersion);
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to record an unsuccessful attempt to check the uploader\'s version', () => {
+ const err = new Error('API error');
+ const expectedAction = {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err,
+ meta: {source: actionSources[actionTypes.VERSION_CHECK_FAILURE]}
+ };
+
+ expect(syncActions.versionCheckFailure(err)).to.deep.equal(expectedAction);
+ });
+
+ it('should create an action to mark the current uploader\'s version as unsupported', () => {
+ const expectedAction = {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: new UnsupportedError(currentVersion, requiredVersion),
+ meta: {
+ source: actionSources[actionTypes.VERSION_CHECK_FAILURE],
+ metric: {
+ eventName: metrics.VERSION_CHECK_FAILURE_OUTDATED,
+ properties: { requiredVersion }
+ }
+ }
+ };
+
+ expect(syncActions.versionCheckFailure(null, currentVersion, requiredVersion)).to.deep.equal(expectedAction);
+ });
+ });
+ });
+
+ describe('for retrieveTargetsFromStorage & putUsersTargetsInStorage', () => {
+ describe('putUsersTargetsInStorage', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.putUsersTargetsInStorage();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to announce the side effet of storing users\' targets locally', () => {
+ const expectedAction = {
+ type: actionTypes.STORING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.STORING_USERS_TARGETS]}
+ };
+ expect(syncActions.putUsersTargetsInStorage()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('retrieveUsersTargetsFromStorage', () => {
+ it('should be an FSA', () => {
+ let action = syncActions.retrieveUsersTargetsFromStorage();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to announce the side effet of storing users\' targets locally', () => {
+ const expectedAction = {
+ type: actionTypes.RETRIEVING_USERS_TARGETS,
+ meta: {source: actionSources[actionTypes.RETRIEVING_USERS_TARGETS]}
+ };
+ expect(syncActions.retrieveUsersTargetsFromStorage()).to.deep.equal(expectedAction);
+ });
+ });
+
+ describe('setUsersTargets', () => {
+ // NB: this is not what this object actually looks like
+ // actual shape is irrelevant to testing action creators
+ const targets = {
+ a1b2c3: ['foo', 'bar'],
+ d4e5f6: ['baz']
+ };
+ it('should be an FSA', () => {
+ let action = syncActions.setUsersTargets();
+
+ expect(isFSA(action)).to.be.true;
+ });
+
+ it('should create an action to set the users\' target devices', () => {
+ const expectedAction = {
+ type: actionTypes.SET_USERS_TARGETS,
+ payload: { targets },
+ meta: {source: actionSources[actionTypes.SET_USERS_TARGETS]}
+ };
+ expect(syncActions.setUsersTargets(targets)).to.deep.equal(expectedAction);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/browser/redux/reducers/misc.test.js b/test/browser/redux/reducers/misc.test.js
new file mode 100644
index 0000000000..29d962e7cd
--- /dev/null
+++ b/test/browser/redux/reducers/misc.test.js
@@ -0,0 +1,397 @@
+/*
+ * == BSD2 LICENSE ==
+ * Copyright (c) 2015-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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/*eslint-env mocha*/
+
+import _ from 'lodash';
+
+import * as actionTypes from '../../../../lib/redux/constants/actionTypes';
+import { pages, steps } from '../../../../lib/redux/constants/otherConstants';
+import * as misc from '../../../../lib/redux/reducers/misc';
+
+import devices from '../../../../lib/redux/reducers/devices';
+
+import { UnsupportedError } from '../../../../lib/redux/utils/errors';
+
+let pwd = require('../../fixtures/pwd.json');
+let nonpwd = require('../../fixtures/nonpwd.json');
+
+describe('misc reducers', () => {
+ describe('devices', () => {
+ function filterDevicesFn(os) {
+ return function(device) {
+ if (device.enabled[os] === true) {
+ return true;
+ }
+ return false;
+ };
+ }
+ it('should return the initial state', () => {
+ expect(misc.devices(undefined, {})).to.deep.equal(devices);
+ });
+
+ it('should handle HIDE_UNAVAILABLE_DEVICES [mac]', () => {
+ let actualResult = misc.devices(undefined, {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'mac'}
+ });
+ let expectedResult = _.pick(devices, filterDevicesFn('mac'));
+ expect(actualResult).to.deep.equal(expectedResult);
+ // because we do currently have devices unavailable on Mac
+ expect(Object.keys(actualResult).length).to.be.lessThan(Object.keys(devices).length);
+ // test to be sure not *mutating* state object but rather returning new!
+ let prevState = devices;
+ let resultState = misc.devices(prevState, {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'mac'}
+ });
+ expect(prevState === resultState).to.be.false;
+ });
+
+ it('should handle HIDE_UNAVAILABLE_DEVICES [win]', () => {
+ let actualResult = misc.devices(undefined, {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'win'}
+ });
+ let expectedResult = _.pick(devices, filterDevicesFn('win'));
+ expect(actualResult).to.deep.equal(expectedResult);
+ // because nothing currently is unavailable on Windows
+ expect(Object.keys(actualResult).length).to.equal(Object.keys(devices).length);
+ // test to be sure not *mutating* state object but rather returning new!
+ let prevState = devices;
+ let resultState = misc.devices(prevState, {
+ type: actionTypes.HIDE_UNAVAILABLE_DEVICES,
+ payload: {os: 'win'}
+ });
+ expect(prevState === resultState).to.be.false;
+ });
+ });
+
+ describe('dropdown', () => {
+ it('should return the initial state', () => {
+ expect(misc.dropdown(undefined, {})).to.be.false;
+ });
+
+ it('should handle TOGGLE_DROPDOWN', () => {
+ expect(misc.dropdown(undefined, {
+ type: actionTypes.TOGGLE_DROPDOWN,
+ payload: {isVisible: true}
+ })).to.be.true;
+ expect(misc.dropdown(undefined, {
+ type: actionTypes.TOGGLE_DROPDOWN,
+ payload: {isVisible: false}
+ })).to.be.false;
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ expect(misc.dropdown(undefined, {
+ type: actionTypes.LOGOUT_REQUEST
+ })).to.be.false;
+ expect(misc.dropdown(true, {
+ type: actionTypes.LOGOUT_REQUEST
+ })).to.be.false;
+ expect(misc.dropdown(false, {
+ type: actionTypes.LOGOUT_REQUEST
+ })).to.be.false;
+ });
+ });
+
+ describe('os', () => {
+ it('should return the initial state', () => {
+ expect(misc.os(undefined, {})).to.be.null;
+ });
+
+ it('should handle SET_OS', () => {
+ expect(misc.os(undefined, {
+ type: actionTypes.SET_OS,
+ payload: {os: 'test'}
+ })).to.equal('test');
+ });
+ });
+
+ describe('page', () => {
+ it('should return the initial state', () => {
+ expect(misc.page(undefined, {})).to.equal(pages.LOADING);
+ });
+
+ it('should handle SET_PAGE', () => {
+ expect(misc.page(undefined, {
+ type: actionTypes.SET_PAGE,
+ payload: {page: 'main'}
+ })).to.equal('main');
+ });
+ });
+
+ describe('unsupported', () => {
+ it('should return the initial state', () => {
+ expect(misc.unsupported(undefined, {})).to.be.true;
+ });
+
+ it('should handle INIT_APP_FAILURE', () => {
+ const err = new Error('Offline!');
+ expect(misc.unsupported(undefined, {
+ type: actionTypes.INIT_APP_FAILURE,
+ error: true,
+ payload: err
+ })).to.deep.equal(err);
+ });
+
+ it('should handle VERSION_CHECK_FAILURE [API error]', () => {
+ const err = new Error('API error!');
+ expect(misc.unsupported(undefined, {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err
+ })).to.deep.equal(err);
+ });
+
+ it('should handle VERSION_CHECK_FAILURE [uploader version doesn\'t meet minimum]', () => {
+ const currentVersion = '0.99.0', requiredVersion = '0.100.0';
+ const err = new UnsupportedError(currentVersion, requiredVersion);
+ expect(misc.unsupported(undefined, {
+ type: actionTypes.VERSION_CHECK_FAILURE,
+ error: true,
+ payload: err
+ })).to.be.true;
+ });
+
+ it('should handle VERSION_CHECK_SUCCESS', () => {
+ expect(misc.unsupported(undefined, {
+ type: actionTypes.VERSION_CHECK_SUCCESS
+ })).to.be.false;
+ });
+ });
+
+ describe('blipUrls', () => {
+ it('should return the initial state', () => {
+ expect(misc.blipUrls(undefined, {})).to.deep.equal({});
+ });
+
+ it('should handle SET_BLIP_VIEW_DATA_URL', () => {
+ const VIEW_DATA_LINK = 'http://www.acme.com/patients/a1b2c3/data';
+ const actionPayload = {url: VIEW_DATA_LINK};
+ expect(misc.blipUrls(undefined, {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: actionPayload
+ }).viewDataLink).to.equal(VIEW_DATA_LINK);
+ // test to be sure not *mutating* state object but rather returning new!
+ let initialState = {};
+ let finalState = misc.blipUrls(initialState, {
+ type: actionTypes.SET_BLIP_VIEW_DATA_URL,
+ payload: actionPayload
+ });
+ expect(initialState === finalState).to.be.false;
+ });
+
+ it('should handle SET_FORGOT_PASSWORD_URL', () => {
+ const FORGOT_PWD = 'http://www.acme.com/forgot-password';
+ const actionPayload = {url: FORGOT_PWD};
+ expect(misc.blipUrls(undefined, {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: actionPayload
+ }).forgotPassword).to.equal(FORGOT_PWD);
+ // test to be sure not *mutating* state object but rather returning new!
+ let initialState = {};
+ let finalState = misc.blipUrls(initialState, {
+ type: actionTypes.SET_FORGOT_PASSWORD_URL,
+ payload: actionPayload
+ });
+ expect(initialState === finalState).to.be.false;
+ });
+
+ it('should handle SET_SIGNUP_URL', () => {
+ const SIGN_UP = 'http://www.acme.com/sign-up';
+ const actionPayload = {url: SIGN_UP};
+ expect(misc.blipUrls(undefined, {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: actionPayload
+ }).signUp).to.equal(SIGN_UP);
+ // test to be sure not *mutating* state object but rather returning new!
+ let initialState = {};
+ let finalState = misc.blipUrls(initialState, {
+ type: actionTypes.SET_SIGNUP_URL,
+ payload: actionPayload
+ });
+ expect(initialState === finalState).to.be.false;
+ });
+ });
+
+ describe('working', () => {
+ it('should return the initial state', () => {
+ expect(misc.working(undefined, {})).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle INIT_APP_FAILURE', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.INIT_APP_FAILURE
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: false,
+ uploading: false
+ });
+ });
+
+ it('should handle INIT_APP_REQUEST', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.INIT_APP_REQUEST
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle INIT_APP_SUCCESS', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.INIT_APP_SUCCESS
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: false,
+ uploading: false
+ });
+ });
+
+ it('should handle LOGIN_FAILURE', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.LOGIN_FAILURE
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle LOGIN_REQUEST', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.LOGIN_REQUEST
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: true,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle LOGIN_SUCCESS', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.LOGIN_SUCCESS
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle READ_FILE_ABORTED', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.READ_FILE_ABORTED
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle READ_FILE_FAILURE', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.READ_FILE_FAILURE
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle VERSION_CHECK_FAILURE', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.VERSION_CHECK_FAILURE
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle VERSION_CHECK_REQUEST', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.VERSION_CHECK_REQUEST
+ })).to.deep.equal({
+ checkingVersion: true,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle VERSION_CHECK_SUCCESS', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.VERSION_CHECK_SUCCESS
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle UPLOAD_FAILURE', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.UPLOAD_FAILURE
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+
+ it('should handle UPLOAD_REQUEST', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.UPLOAD_REQUEST
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: true
+ });
+ });
+
+ it('should handle UPLOAD_SUCCESS', () => {
+ expect(misc.working(undefined, {
+ type: actionTypes.UPLOAD_SUCCESS
+ })).to.deep.equal({
+ checkingVersion: false,
+ fetchingUserInfo: false,
+ initializingApp: true,
+ uploading: false
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/browser/redux/reducers/uploads.test.js b/test/browser/redux/reducers/uploads.test.js
new file mode 100644
index 0000000000..e211fec306
--- /dev/null
+++ b/test/browser/redux/reducers/uploads.test.js
@@ -0,0 +1,691 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/*eslint-env mocha*/
+
+import _ from 'lodash';
+
+import * as actionTypes from '../../../../lib/redux/constants/actionTypes';
+import { steps } from '../../../../lib/redux/constants/otherConstants';
+import * as uploads from '../../../../lib/redux/reducers/uploads';
+
+describe('uploads', () => {
+ describe('uploadProgress', () => {
+ it('should return the initial state', () => {
+ expect(uploads.uploadProgress(undefined, {})).to.be.null;
+ });
+
+ it('should handle CARELINK_FETCH_REQUEST', () => {
+ let initialState = {
+ percentage: 0,
+ step: steps.start
+ };
+ let result = uploads.uploadProgress(initialState, {
+ type: actionTypes.CARELINK_FETCH_REQUEST
+ });
+ expect(result).to.deep.equal({
+ percentage: 0,
+ step: steps.carelinkFetch
+ });
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle DEVICE_DETECT_REQUEST', () => {
+ let initialState = {
+ percentage: 0,
+ step: steps.start
+ };
+ let result = uploads.uploadProgress(initialState, {
+ type: actionTypes.DEVICE_DETECT_REQUEST
+ });
+ expect(result).to.deep.equal({
+ percentage: 0,
+ step: steps.detect
+ });
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle UPLOAD_FAILURE', () => {
+ const initialState = {
+ percentage: 0,
+ step: steps.detect
+ };
+ expect(uploads.uploadProgress(initialState, {
+ type: actionTypes.UPLOAD_FAILURE
+ })).to.be.null;
+ });
+
+ it('should handle UPLOAD_PROGRESS', () => {
+ let initialState = {
+ percentage: 0,
+ step: steps.detect
+ };
+ let result = uploads.uploadProgress(initialState, {
+ type: actionTypes.UPLOAD_PROGRESS,
+ payload: {percentage: 65, step: steps.uploadData}
+ });
+ expect(result).to.deep.equal({
+ percentage: 65,
+ step: steps.uploadData
+ });
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle UPLOAD_REQUEST', () => {
+ expect(uploads.uploadProgress(undefined, {
+ type: actionTypes.UPLOAD_REQUEST
+ })).to.deep.equal({
+ percentage: 0,
+ step: steps.start
+ });
+ });
+
+ it('should handle UPLOAD_SUCCESS', () => {
+ const initialState = {
+ percentage: 100,
+ step: steps.cleanup
+ };
+ expect(uploads.uploadProgress(initialState, {
+ type: actionTypes.UPLOAD_SUCCESS
+ })).to.be.null;
+ });
+ });
+
+ describe('uploadsByUser', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_cgm';
+ const filename = 'data.csv';
+ const time = '2016-01-01T12:05:00.123Z';
+ it('should return the initial state', () => {
+ expect(uploads.uploadsByUser(undefined, {})).to.deep.equal({});
+ });
+
+ it('should handle CARELINK_FETCH_FAILURE', () => {
+ let initialState = {
+ [userId]: {
+ carelink: {history: [{start: time}], isFetching: true}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.CARELINK_FETCH_FAILURE
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ carelink: {history: [{start: time}], isFetching: false}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.carelink === result.a1b2c3.carelink).to.be.false;
+ });
+
+ it('should handle CARELINK_FETCH_REQUEST', () => {
+ let initialState = {
+ [userId]: {
+ carelink: {history: [{start: time}]}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.CARELINK_FETCH_REQUEST,
+ payload: { userId, deviceKey: 'carelink' }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ carelink: {history: [{start: time}], isFetching: true}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.carelink === result.a1b2c3.carelink).to.be.false;
+ });
+
+ it('should handle CARELINK_FETCH_SUCCESS', () => {
+ let initialState = {
+ [userId]: {
+ carelink: {history: [{start: time}], isFetching: true}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.CARELINK_FETCH_SUCCESS,
+ payload: { userId, deviceKey: 'carelink' }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ carelink: {history: [{start: time}], isFetching: false}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.carelink === result.a1b2c3.carelink).to.be.false;
+ });
+
+ it('should handle CHOOSING_FILE', () => {
+ let initialState = {
+ [userId]: {
+ a_pump: {history: [1,2]},
+ [deviceKey]: {history: []}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ a_pump: {disabled: true, history: [1,2]},
+ [deviceKey]: {choosingFile: true, history: []}
+ },
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle READ_FILE_ABORTED', () => {
+ const err = new Error('Wrong file ext!');
+ let initialState = {
+ [userId]: {[deviceKey]: {choosingFile: true, history: []}}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.READ_FILE_ABORTED,
+ error: true,
+ payload: err
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ completed: true,
+ error: err,
+ failed: true,
+ history: []
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle READ_FILE_FAILURE', () => {
+ const err = new Error('File read error!');
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ file: {name: filename},
+ history: [],
+ readingFile: true
+ }}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.READ_FILE_FAILURE,
+ error: true,
+ payload: err
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ completed: true,
+ error: err,
+ failed: true,
+ file: {name: filename},
+ history: [],
+ readingFile: false
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle READ_FILE_REQUEST', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {choosingFile: true, history: []}}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.READ_FILE_REQUEST,
+ payload: { userId, deviceKey, filename }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ file: {name: filename},
+ history: [],
+ readingFile: true
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle READ_FILE_SUCCESS', () => {
+ const filedata = [1,2,3,4,5];
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ file: {name: filename},
+ history: [],
+ readingFile: true
+ }}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.READ_FILE_SUCCESS,
+ payload: { userId, deviceKey, filedata }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ file: {name: filename, data: filedata},
+ history: [],
+ readingFile: false
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ expect(initialState.a1b2c3.a_cgm.file === result.a1b2c3.a_cgm.file).to.be.false;
+ });
+
+ it('should handle RESET_UPLOAD [another upload in progress]', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ completed: true,
+ error: new Error(),
+ failed: true,
+ history: [{start: time, finish: time, error: true}]
+ },
+ another_pump: {
+ history: [{start: time}],
+ uploading: true
+ }
+ }};
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ disabled: true,
+ history: [{start: time, finish: time, error: true}]
+ },
+ another_pump: {
+ history: [{start: time}],
+ uploading: true
+ }
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle RESET_UPLOAD [resetting block mode w/file data]', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ choosingFile: false,
+ completed: true,
+ data: [2,4,6],
+ file: {data: [1,2,3], name: 'foo.ibf'},
+ history: [{start: time, finish: time}],
+ readingFile: false,
+ successful: true,
+ uploading: false
+ },
+ another_pump: {
+ history: [5,10]
+ }
+ }};
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ history: [{start: time, finish: time}]
+ },
+ another_pump: {
+ history: [5,10]
+ }
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle RESET_UPLOAD [resetting another when block mode successful not reset]', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ completed: true,
+ data: [8,10],
+ history: [{start: time, finish: time}],
+ successful: true,
+ uploading: false
+ },
+ another_pump: {
+ choosingFile: false,
+ completed: true,
+ data: [2,4,6],
+ file: {data: [1,2,3], name: 'foo.ibf'},
+ history: [{start: time, finish: time}],
+ readingFile: false,
+ successful: true,
+ uploading: false
+ }
+ }};
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ history: [{start: time, finish: time}]
+ },
+ another_pump: {
+ choosingFile: false,
+ completed: true,
+ data: [2,4,6],
+ file: {data: [1,2,3], name: 'foo.ibf'},
+ history: [{start: time, finish: time}],
+ readingFile: false,
+ successful: true,
+ uploading: false
+ }
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle RESET_UPLOAD [upload failed]', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ completed: true,
+ error: new Error(),
+ failed: true,
+ history: [{start: time, finish: time, error: true}]
+ }}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ history: [{start: time, finish: time, error: true}]
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle RESET_UPLOAD [upload successful]', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {
+ completed: true,
+ history: [{start: time, finish: time}],
+ successful: true
+ }}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.RESET_UPLOAD,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {
+ history: [{start: time, finish: time}]
+ }}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle SET_UPLOADS', () => {
+ let initialState = {
+ a1b2c3: {a_meter: {history: [1]}, a_cgm: {completed: true, history: [1,2,3]}}
+ };
+ const actionPayload = {
+ devicesByUser: {
+ a1b2c3: ['a_pump', 'a_cgm'],
+ d4e5f6: ['another_pump']
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.SET_UPLOADS,
+ payload: actionPayload
+ });
+ expect(result).to.deep.equal({
+ a1b2c3: {
+ a_pump: {history: []},
+ a_cgm: {completed: true, history: [1,2,3]}
+ },
+ d4e5f6: {
+ another_pump: {history: []}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ });
+
+ it('should handle TOGGLE_ERROR_DETAILS', () => {
+ let initialState = {
+ [userId]: {[deviceKey]: {history: []}}
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.TOGGLE_ERROR_DETAILS,
+ payload: { userId, deviceKey, isVisible: true }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {[deviceKey]: {history: [], showErrorDetails: true}}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ let result2 = uploads.uploadsByUser(initialState, {
+ type: actionTypes.TOGGLE_ERROR_DETAILS,
+ payload: { userId, deviceKey, isVisible: false }
+ });
+ expect(result2).to.deep.equal({
+ [userId]: {[deviceKey]: {history: [], showErrorDetails: false}}
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result2).to.be.false;
+ expect(initialState.a1b2c3 === result2.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result2.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle UPLOAD_FAILURE', () => {
+ let err = new Error('Upload Error!');
+ err.utc = time;
+ let initialState = {
+ [userId]: {
+ a_pump: {disabled: true, history: []},
+ [deviceKey]: {
+ history: [{start: time},1,2,3],
+ uploading: true
+ }
+ },
+ d4e5f6: {
+ another_pump: {history: []}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.UPLOAD_FAILURE,
+ error: true,
+ payload: err
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ a_pump: {history: []},
+ [deviceKey]: {
+ completed: true,
+ error: err,
+ failed: true,
+ history: [{start: time, finish: time, error: true}, 1, 2, 3],
+ uploading: false
+ }
+ },
+ d4e5f6: {
+ another_pump: {history: []}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ expect(initialState.a1b2c3.a_cgm.history === result.a1b2c3.a_cgm.history).to.be.false;
+ expect(initialState.a1b2c3.a_cgm.history[0] === result.a1b2c3.a_cgm.history[0]).to.be.false;
+ });
+
+ it('should handle UPLOAD_REQUEST', () => {
+ let initialState = {
+ [userId]: {
+ a_pump: {history: []},
+ [deviceKey]: {history: [1,2,3]}
+ }
+ };
+ const actionPayload = { userId, deviceKey };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time }
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ a_pump: {disabled: true, history: []},
+ [deviceKey]: {
+ history: [{start: time},1,2,3],
+ uploading: true
+ }
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ });
+
+ it('should handle UPLOAD_SUCCESS', () => {
+ const data = [1,2,3,4,5];
+ let initialState = {
+ [userId]: {
+ a_pump: {disabled: true, history: []},
+ [deviceKey]: {
+ history: [{start: time},1,2,3],
+ uploading: true
+ }
+ },
+ d4e5f6: {
+ another_pump: {history: []}
+ }
+ };
+ let result = uploads.uploadsByUser(initialState, {
+ type: actionTypes.UPLOAD_SUCCESS,
+ payload: { userId, deviceKey, data, utc: time}
+ });
+ expect(result).to.deep.equal({
+ [userId]: {
+ a_pump: {history: []},
+ [deviceKey]: {
+ completed: true,
+ data: data,
+ history: [{start: time, finish: time}, 1, 2, 3],
+ successful: true,
+ uploading: false
+ }
+ },
+ d4e5f6: {
+ another_pump: {history: []}
+ }
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.a1b2c3 === result.a1b2c3).to.be.false;
+ expect(initialState.a1b2c3.a_cgm === result.a1b2c3.a_cgm).to.be.false;
+ expect(initialState.a1b2c3.a_cgm.history === result.a1b2c3.a_cgm.history).to.be.false;
+ expect(initialState.a1b2c3.a_cgm.history[0] === result.a1b2c3.a_cgm.history[0]).to.be.false;
+ });
+ });
+
+ describe('uploadTargetDevice', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ const time = '2016-01-01T12:05:00.123Z';
+ it('should return the initial state', () => {
+ expect(uploads.uploadTargetDevice(undefined, {})).to.be.null;
+ });
+
+ it('should handle CHOOSING_FILE', () => {
+ expect(uploads.uploadTargetDevice(undefined, {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { deviceKey }
+ })).to.equal(deviceKey);
+ expect(uploads.uploadTargetDevice('a_cgm', {
+ type: actionTypes.CHOOSING_FILE,
+ payload: { deviceKey }
+ })).to.equal(deviceKey);
+ });
+
+ it('should handle READ_FILE_ABORTED', () => {
+ expect(uploads.uploadTargetDevice(deviceKey, {
+ type: actionTypes.READ_FILE_ABORTED
+ })).to.be.null;
+ });
+
+ it('should handle READ_FILE_FAILURE', () => {
+ expect(uploads.uploadTargetDevice(deviceKey, {
+ type: actionTypes.READ_FILE_FAILURE
+ })).to.be.null;
+ });
+
+ it('should handle UPLOAD_FAILURE', () => {
+ expect(uploads.uploadTargetDevice('a_pump', {
+ type: actionTypes.UPLOAD_FAILURE
+ })).to.be.null;
+ });
+
+ it('should handle UPLOAD_REQUEST', () => {
+ expect(uploads.uploadTargetDevice(undefined, {
+ type: actionTypes.UPLOAD_REQUEST,
+ payload: { userId, deviceKey, utc: time}
+ })).to.equal(deviceKey);
+ });
+
+ it('should handle UPLOAD_SUCCESS', () => {
+ expect(uploads.uploadTargetDevice('a_pump', {
+ type: actionTypes.UPLOAD_SUCCESS
+ })).to.be.null;
+ });
+ });
+});
diff --git a/test/browser/redux/reducers/users.test.js b/test/browser/redux/reducers/users.test.js
new file mode 100644
index 0000000000..8a0e81c34d
--- /dev/null
+++ b/test/browser/redux/reducers/users.test.js
@@ -0,0 +1,510 @@
+/*
+ * == BSD2 LICENSE ==
+ * 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
+ * License as published by the Open Source Initiative at opensource.org.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the License for more details.
+ *
+ * You should have received a copy of the License along with this program; if
+ * not, you can obtain one from Tidepool Project at tidepool.org.
+ * == BSD2 LICENSE ==
+ */
+
+/*eslint-env mocha*/
+
+import _ from 'lodash';
+
+import * as actionTypes from '../../../../lib/redux/constants/actionTypes';
+import * as users from '../../../../lib/redux/reducers/users';
+
+describe('users', () => {
+ describe('allUsers', () => {
+ const user = {userid: 'a1b2c3', email: 'annie@foo.com'};
+ const profile = {fullName: 'Annie Foo'};
+ const memberships = [
+ {userid: 'a1b2c3', profile: {fullName: 'Annie Foo'}},
+ {userid: 'd4e5f6', profile: {b: 2}}
+ ];
+ it('should return the initial state', () => {
+ expect(users.allUsers(undefined, {})).to.deep.equal({});
+ });
+
+ it('should handle LOGIN_SUCCESS', () => {
+ const action = {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships }
+ };
+ expect(users.allUsers(undefined, action)).to.deep.equal({
+ a1b2c3: {email: user.email, fullName: profile.fullName},
+ d4e5f6: {b: 2}
+ });
+ let initialState = {};
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === users.allUsers(initialState, action)).to.be.false;
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN', () => {
+ const action = {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships }
+ };
+ expect(users.allUsers(undefined, action)).to.deep.equal({
+ a1b2c3: {email: user.email, fullName: profile.fullName},
+ d4e5f6: {b: 2}
+ });
+ let initialState = {};
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === users.allUsers(initialState, action)).to.be.false;
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ let initialState = {foo: 'bar'};
+ let result = users.allUsers(initialState, {
+ type: actionTypes.LOGOUT_REQUEST
+ });
+ expect(result).to.deep.equal({});
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+ });
+
+ describe('loggedInUser', () => {
+ const user = {userid: 'a1b2c3'};
+ it('should return the initial state', () => {
+ expect(users.loggedInUser(undefined, {})).to.be.null;
+ });
+
+ it('should handle LOGIN_SUCCESS', () => {
+ expect(users.loggedInUser(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user }
+ })).to.equal(user.userid);
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ expect(users.loggedInUser(undefined, {
+ type: actionTypes.LOGOUT_REQUEST
+ })).to.be.null;
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN', () => {
+ expect(users.loggedInUser(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user }
+ })).to.equal(user.userid);
+ });
+ });
+
+ describe('loginErrorMessage', () => {
+ it('should return the initial state', () => {
+ expect(users.loginErrorMessage(undefined, {})).to.be.null;
+ });
+
+ it('should handle LOGIN_FAILURE', () => {
+ const errMsg = 'Login error!';
+ expect(users.loginErrorMessage(undefined, {
+ type: actionTypes.LOGIN_FAILURE,
+ error: true,
+ payload: new Error(errMsg)
+ })).to.equal(errMsg);
+ });
+
+ it('should handle LOGIN_REQUEST', () => {
+ expect(users.loginErrorMessage(undefined, {
+ type: actionTypes.LOGIN_REQUEST
+ })).to.be.null;
+ });
+ });
+
+ describe('targetDevices', () => {
+ const memberships = [
+ {userid: 'a1b2c3', profile: {foo: 'bar'}},
+ {userid: 'd4e5f6', profile: {patient: {a: 1}}},
+ {userid: 'g7h8i0', profile: {patient: {b: 2}}}
+ ];
+ it('should return the initial state', () => {
+ expect(users.targetDevices(undefined, {})).to.deep.equal({});
+ });
+
+ it('should handle ADD_TARGET_DEVICE', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ let initialState = {
+ [userId]: ['a_meter'],
+ d4e5f6: ['another_pump']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.ADD_TARGET_DEVICE,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState[userId] === result[userId]).to.be.false;
+ });
+
+ it('should handle ADD_TARGET_DEVICE [without dups]', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_pump';
+ let initialState = {
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.ADD_TARGET_DEVICE,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState[userId] === result[userId]).to.be.false;
+ });
+
+ it('should handle ADD_TARGET_DEVICE [when no user selected]', () => {
+ const userId = 'noUserSelected', deviceKey = 'a_pump';
+ let initialState = {
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.ADD_TARGET_DEVICE,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ noUserSelected: ['a_pump'],
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState[userId] === result[userId]).to.be.false;
+ });
+
+ it('should handle LOGIN_SUCCESS', () => {
+ expect(users.targetDevices(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { memberships }
+ })).to.deep.equal({
+ d4e5f6: [],
+ g7h8i0: []
+ });
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ let initialState = {
+ d4e5f6: ['a_meter', 'another_pump'],
+ g7h8i0: ['a_pump', 'a_cgm']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.LOGOUT_REQUEST
+ });
+ expect(result).to.deep.equal({});
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle REMOVE_TARGET_DEVICE', () => {
+ const userId = 'a1b2c3', deviceKey = 'a_meter';
+ let initialState = {
+ [userId]: ['a_meter', 'a_pump'],
+ d4e5f6: ['another_pump']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.REMOVE_TARGET_DEVICE,
+ payload: { userId, deviceKey }
+ });
+ expect(result).to.deep.equal({
+ [userId]: ['a_pump'],
+ d4e5f6: ['another_pump']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState[userId] === result[userId]).to.be.false;
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN', () => {
+ expect(users.targetDevices(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { memberships }
+ })).to.deep.equal({
+ d4e5f6: [],
+ g7h8i0: []
+ });
+ });
+
+ it('should handle SET_USERS_TARGETS', () => {
+ let initialState = {
+ d4e5f6: [],
+ g7h8i0: []
+ };
+ const targets = {
+ d4e5f6: [{key: 'a_cgm'}],
+ g7h8i0: [{key: 'a_pump'}, {key: 'a_meter'}],
+ j1k2l3: [{key: 'another_pump'}]
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.SET_USERS_TARGETS,
+ payload: { targets }
+ });
+ expect(result).to.deep.equal({
+ d4e5f6: ['a_cgm'],
+ g7h8i0: ['a_pump', 'a_meter']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.d4e5f6 === result.d4e5f6).to.be.false;
+ expect(initialState.g7h8i0 === result.g7h8i0).to.be.false;
+ });
+
+ it('should handle STORING_USERS_TARGETS (by clearing noUserSelected devices)', () => {
+ const initialState = {
+ noUserSelected: ['a_pump', 'a_cgm'],
+ a1b2c3: ['a_pump', 'a_cgm', 'a_meter']
+ };
+ let result = users.targetDevices(initialState, {
+ type: actionTypes.STORING_USERS_TARGETS
+ });
+ expect(result).to.deep.equal({
+ a1b2c3: ['a_pump', 'a_cgm', 'a_meter']
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+ });
+
+ describe('targetTimezones', () => {
+ const memberships = [
+ {userid: 'a1b2c3', profile: {foo: 'bar'}},
+ {userid: 'd4e5f6', profile: {patient: {a: 1}}},
+ {userid: 'g7h8i0', profile: {patient: {b: 2}}}
+ ];
+ it('should return the initial state', () => {
+ expect(users.targetTimezones(undefined, {})).to.deep.equal({});
+ });
+
+ it('should handle LOGIN_SUCCESS', () => {
+ expect(users.targetTimezones(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { memberships }
+ })).to.deep.equal({
+ d4e5f6: null,
+ g7h8i0: null
+ });
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ let initialState = {
+ d4e5f6: 'Pacific/Honolulu',
+ g7h8i0: 'US/Pacific'
+ };
+ let result = users.targetTimezones(initialState, {
+ type: actionTypes.LOGOUT_REQUEST
+ });
+ expect(result).to.deep.equal({});
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle SET_TARGET_TIMEZONE', () => {
+ const userId = 'a1b2c3', timezoneName = 'Pacific/Honolulu';
+ let initialState = {
+ [userId]: null,
+ d4e5f6: 'US/Pacific'
+ };
+ let result = users.targetTimezones(initialState, {
+ type: actionTypes.SET_TARGET_TIMEZONE,
+ payload: { userId, timezoneName }
+ });
+ expect(result).to.deep.equal({
+ [userId]: timezoneName,
+ d4e5f6: 'US/Pacific'
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState[userId] === result[userId]).to.be.false;
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN', () => {
+ expect(users.targetTimezones(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { memberships }
+ })).to.deep.equal({
+ d4e5f6: null,
+ g7h8i0: null
+ });
+ });
+
+ it('should handle SET_USERS_TARGETS', () => {
+ let initialState = {
+ d4e5f6: null,
+ g7h8i0: null
+ };
+ const targets = {
+ d4e5f6: [{timezone: 'Pacific/Honolulu'}],
+ g7h8i0: [{timezone: 'Europe/London'}, {timezone: 'Pacific/Auckland'}],
+ j1k2l3: [{timezone: 'US/Eastern'}]
+ };
+ let result = users.targetTimezones(initialState, {
+ type: actionTypes.SET_USERS_TARGETS,
+ payload: { targets }
+ });
+ expect(result).to.deep.equal({
+ d4e5f6: 'Pacific/Honolulu',
+ g7h8i0: null
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ expect(initialState.d4e5f6 === result.d4e5f6).to.be.false;
+ });
+
+ it('should handle STORING_USERS_TARGETS (by clearing noUserSelected devices)', () => {
+ const initialState = {
+ noUserSelected: 'Pacific/Honolulu',
+ a1b2c3: 'US/Eastern'
+ };
+ let result = users.targetTimezones(initialState, {
+ type: actionTypes.STORING_USERS_TARGETS
+ });
+ expect(result).to.deep.equal({
+ a1b2c3: 'US/Eastern'
+ });
+ // tests to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+ });
+
+ describe('targetUsersForUpload', () => {
+ const user = {userid: 'a1b2c3'};
+ const memberships = [
+ {userid: 'a1b2c3', profile: {fullName: 'Annie Foo'}},
+ {userid: 'd4e5f6', profile: {patient: {b: 2}}}
+ ];
+ it('should return the initial state', () => {
+ expect(users.targetUsersForUpload(undefined, {})).to.deep.equal([]);
+ });
+
+ it('should handle LOGIN_SUCCESS [loggedInUser is PWD]', () => {
+ const profile = {patient: {diagnosisDate: '1999-01-01'}};
+ expect(users.targetUsersForUpload(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships }
+ })).to.deep.equal(['a1b2c3', 'd4e5f6']);
+ });
+
+ it('should handle LOGIN_SUCCESS [loggedInUser is not PWD]', () => {
+ const profile = {a: 1};
+ expect(users.targetUsersForUpload(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships }
+ })).to.deep.equal(['d4e5f6']);
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ const initialState = ['d4e5f6'];
+ const result = users.targetUsersForUpload(initialState, {
+ type: actionTypes.LOGOUT_REQUEST
+ });
+ expect(result).to.deep.equal([]);
+ // test to be sure not *mutating* state object but rather returning new!
+ expect(initialState === result).to.be.false;
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN [loggedInUser is PWD]', () => {
+ const profile = {patient: {diagnosisDate: '1999-01-01'}};
+ expect(users.targetUsersForUpload(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships }
+ })).to.deep.equal(['a1b2c3', 'd4e5f6']);
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN [loggedInUser is not PWD]', () => {
+ const profile = {a: 1};
+ expect(users.targetUsersForUpload(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships }
+ })).to.deep.equal(['d4e5f6']);
+ });
+ });
+
+ describe('uploadTargetUser', () => {
+ const user = {userid: 'a1b2c3'};
+ it('should return the initial state', () => {
+ expect(users.uploadTargetUser(undefined, {})).to.be.null;
+ });
+
+ it('should handle LOGIN_SUCCESS [loggedInUser is PWD]', () => {
+ const profile = {patient: {diagnosisDate: '1999-01-01'}};
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile }
+ })).to.equal(user.userid);
+ });
+
+ it('should handle LOGIN_SUCCESS [loggedInUser is not PWD, can upload to only one]', () => {
+ const profile = {a: 1};
+ const memberships = [{userid: 'd4e5f6'}];
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships }
+ })).to.equal(memberships[0].userid);
+ });
+
+ it('should handle LOGIN_SUCCESS [loggedInUser is not PWD, can upload to > 1]', () => {
+ const profile = {a: 1};
+ const memberships = [{userid: 'd4e5f6'}, {foo: 'bar'}];
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.LOGIN_SUCCESS,
+ payload: { user, profile, memberships }
+ })).to.be.null;
+ });
+
+ it('should handle LOGOUT_REQUEST', () => {
+ expect(users.uploadTargetUser('d4e5f6', {
+ type: actionTypes.LOGOUT_REQUEST
+ })).to.equal(null);
+ });
+
+ it('should handle SET_UPLOAD_TARGET_USER', () => {
+ const userId = 'a1b2c3';
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.SET_UPLOAD_TARGET_USER,
+ payload: { userId }
+ })).to.equal(userId);
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN [loggedInUser is PWD]', () => {
+ const profile = {patient: {diagnosisData: '1999-01-01'}};
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile }
+ })).to.equal(user.userid);
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN [loggedInUser is not PWD, can upload to only one]', () => {
+ const profile = {a: 1};
+ const memberships = [{userid: 'd4e5f6'}];
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships }
+ })).to.equal(memberships[0].userid);
+ });
+
+ it('should handle SET_USER_INFO_FROM_TOKEN [loggedInUser is not PWD, can upload to > 1]', () => {
+ const profile = {a: 1};
+ const memberships = [{userid: 'd4e5f6'}, {foo: 'bar'}];
+ expect(users.uploadTargetUser(undefined, {
+ type: actionTypes.SET_USER_INFO_FROM_TOKEN,
+ payload: { user, profile, memberships }
+ })).to.be.null;
+ });
+ });
+});
diff --git a/test/mocha.opts b/test/mocha.opts
deleted file mode 100644
index 63b406d2ed..0000000000
--- a/test/mocha.opts
+++ /dev/null
@@ -1 +0,0 @@
---recursive
\ No newline at end of file
diff --git a/test/carelink/basal/scheduled/normal/input.csv b/test/node/carelink/basal/scheduled/normal/input.csv
similarity index 100%
rename from test/carelink/basal/scheduled/normal/input.csv
rename to test/node/carelink/basal/scheduled/normal/input.csv
diff --git a/test/carelink/basal/scheduled/normal/output.json b/test/node/carelink/basal/scheduled/normal/output.json
similarity index 100%
rename from test/carelink/basal/scheduled/normal/output.json
rename to test/node/carelink/basal/scheduled/normal/output.json
diff --git a/test/carelink/basal/temp/percent/input.csv b/test/node/carelink/basal/temp/percent/input.csv
similarity index 100%
rename from test/carelink/basal/temp/percent/input.csv
rename to test/node/carelink/basal/temp/percent/input.csv
diff --git a/test/carelink/basal/temp/percent/output.json b/test/node/carelink/basal/temp/percent/output.json
similarity index 100%
rename from test/carelink/basal/temp/percent/output.json
rename to test/node/carelink/basal/temp/percent/output.json
diff --git a/test/carelink/basal/temp/rate/input.csv b/test/node/carelink/basal/temp/rate/input.csv
similarity index 100%
rename from test/carelink/basal/temp/rate/input.csv
rename to test/node/carelink/basal/temp/rate/input.csv
diff --git a/test/carelink/basal/temp/rate/output.json b/test/node/carelink/basal/temp/rate/output.json
similarity index 100%
rename from test/carelink/basal/temp/rate/output.json
rename to test/node/carelink/basal/temp/rate/output.json
diff --git a/test/carelink/bolus/22-series/dual/withWizard/old.csv b/test/node/carelink/bolus/22-series/dual/withWizard/old.csv
similarity index 100%
rename from test/carelink/bolus/22-series/dual/withWizard/old.csv
rename to test/node/carelink/bolus/22-series/dual/withWizard/old.csv
diff --git a/test/carelink/bolus/22-series/dual/withWizard/output.json b/test/node/carelink/bolus/22-series/dual/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/22-series/dual/withWizard/output.json
rename to test/node/carelink/bolus/22-series/dual/withWizard/output.json
diff --git a/test/carelink/bolus/22-series/normal/noWizard/old.csv b/test/node/carelink/bolus/22-series/normal/noWizard/old.csv
similarity index 100%
rename from test/carelink/bolus/22-series/normal/noWizard/old.csv
rename to test/node/carelink/bolus/22-series/normal/noWizard/old.csv
diff --git a/test/carelink/bolus/22-series/normal/noWizard/output.json b/test/node/carelink/bolus/22-series/normal/noWizard/output.json
similarity index 100%
rename from test/carelink/bolus/22-series/normal/noWizard/output.json
rename to test/node/carelink/bolus/22-series/normal/noWizard/output.json
diff --git a/test/carelink/bolus/22-series/normal/withWizard/old.csv b/test/node/carelink/bolus/22-series/normal/withWizard/old.csv
similarity index 100%
rename from test/carelink/bolus/22-series/normal/withWizard/old.csv
rename to test/node/carelink/bolus/22-series/normal/withWizard/old.csv
diff --git a/test/carelink/bolus/22-series/normal/withWizard/output.json b/test/node/carelink/bolus/22-series/normal/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/22-series/normal/withWizard/output.json
rename to test/node/carelink/bolus/22-series/normal/withWizard/output.json
diff --git a/test/carelink/bolus/22-series/square/withWizard/old.csv b/test/node/carelink/bolus/22-series/square/withWizard/old.csv
similarity index 100%
rename from test/carelink/bolus/22-series/square/withWizard/old.csv
rename to test/node/carelink/bolus/22-series/square/withWizard/old.csv
diff --git a/test/carelink/bolus/22-series/square/withWizard/output.json b/test/node/carelink/bolus/22-series/square/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/22-series/square/withWizard/output.json
rename to test/node/carelink/bolus/22-series/square/withWizard/output.json
diff --git a/test/carelink/bolus/dual/noWizard/input.csv b/test/node/carelink/bolus/dual/noWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/noWizard/input.csv
rename to test/node/carelink/bolus/dual/noWizard/input.csv
diff --git a/test/carelink/bolus/dual/noWizard/output.json b/test/node/carelink/bolus/dual/noWizard/output.json
similarity index 100%
rename from test/carelink/bolus/dual/noWizard/output.json
rename to test/node/carelink/bolus/dual/noWizard/output.json
diff --git a/test/carelink/bolus/dual/noWizardNoNormal/input.csv b/test/node/carelink/bolus/dual/noWizardNoNormal/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/noWizardNoNormal/input.csv
rename to test/node/carelink/bolus/dual/noWizardNoNormal/input.csv
diff --git a/test/carelink/bolus/dual/noWizardNoNormal/output.json b/test/node/carelink/bolus/dual/noWizardNoNormal/output.json
similarity index 100%
rename from test/carelink/bolus/dual/noWizardNoNormal/output.json
rename to test/node/carelink/bolus/dual/noWizardNoNormal/output.json
diff --git a/test/carelink/bolus/dual/withMultiWizardOutOfSequence/input.csv b/test/node/carelink/bolus/dual/withMultiWizardOutOfSequence/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/withMultiWizardOutOfSequence/input.csv
rename to test/node/carelink/bolus/dual/withMultiWizardOutOfSequence/input.csv
diff --git a/test/carelink/bolus/dual/withMultiWizardOutOfSequence/output.json b/test/node/carelink/bolus/dual/withMultiWizardOutOfSequence/output.json
similarity index 100%
rename from test/carelink/bolus/dual/withMultiWizardOutOfSequence/output.json
rename to test/node/carelink/bolus/dual/withMultiWizardOutOfSequence/output.json
diff --git a/test/carelink/bolus/dual/withWizard/input.csv b/test/node/carelink/bolus/dual/withWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/withWizard/input.csv
rename to test/node/carelink/bolus/dual/withWizard/input.csv
diff --git a/test/carelink/bolus/dual/withWizard/output.json b/test/node/carelink/bolus/dual/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/dual/withWizard/output.json
rename to test/node/carelink/bolus/dual/withWizard/output.json
diff --git a/test/carelink/bolus/dual/withWizardInterruptedWithOverride/input.csv b/test/node/carelink/bolus/dual/withWizardInterruptedWithOverride/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/withWizardInterruptedWithOverride/input.csv
rename to test/node/carelink/bolus/dual/withWizardInterruptedWithOverride/input.csv
diff --git a/test/carelink/bolus/dual/withWizardInterruptedWithOverride/output.json b/test/node/carelink/bolus/dual/withWizardInterruptedWithOverride/output.json
similarity index 100%
rename from test/carelink/bolus/dual/withWizardInterruptedWithOverride/output.json
rename to test/node/carelink/bolus/dual/withWizardInterruptedWithOverride/output.json
diff --git a/test/carelink/bolus/dual/withWizardNoNormal/input.csv b/test/node/carelink/bolus/dual/withWizardNoNormal/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/withWizardNoNormal/input.csv
rename to test/node/carelink/bolus/dual/withWizardNoNormal/input.csv
diff --git a/test/carelink/bolus/dual/withWizardNoNormal/output.json b/test/node/carelink/bolus/dual/withWizardNoNormal/output.json
similarity index 100%
rename from test/carelink/bolus/dual/withWizardNoNormal/output.json
rename to test/node/carelink/bolus/dual/withWizardNoNormal/output.json
diff --git a/test/carelink/bolus/dual/withWizardOutOfSequence/input.csv b/test/node/carelink/bolus/dual/withWizardOutOfSequence/input.csv
similarity index 100%
rename from test/carelink/bolus/dual/withWizardOutOfSequence/input.csv
rename to test/node/carelink/bolus/dual/withWizardOutOfSequence/input.csv
diff --git a/test/carelink/bolus/dual/withWizardOutOfSequence/output.json b/test/node/carelink/bolus/dual/withWizardOutOfSequence/output.json
similarity index 100%
rename from test/carelink/bolus/dual/withWizardOutOfSequence/output.json
rename to test/node/carelink/bolus/dual/withWizardOutOfSequence/output.json
diff --git a/test/carelink/bolus/normal/noWizard/input.csv b/test/node/carelink/bolus/normal/noWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/normal/noWizard/input.csv
rename to test/node/carelink/bolus/normal/noWizard/input.csv
diff --git a/test/carelink/bolus/normal/noWizard/output.json b/test/node/carelink/bolus/normal/noWizard/output.json
similarity index 100%
rename from test/carelink/bolus/normal/noWizard/output.json
rename to test/node/carelink/bolus/normal/noWizard/output.json
diff --git a/test/carelink/bolus/normal/noWizardInterrupted/input.csv b/test/node/carelink/bolus/normal/noWizardInterrupted/input.csv
similarity index 100%
rename from test/carelink/bolus/normal/noWizardInterrupted/input.csv
rename to test/node/carelink/bolus/normal/noWizardInterrupted/input.csv
diff --git a/test/carelink/bolus/normal/noWizardInterrupted/output.json b/test/node/carelink/bolus/normal/noWizardInterrupted/output.json
similarity index 100%
rename from test/carelink/bolus/normal/noWizardInterrupted/output.json
rename to test/node/carelink/bolus/normal/noWizardInterrupted/output.json
diff --git a/test/carelink/bolus/normal/withOverride/input.csv b/test/node/carelink/bolus/normal/withOverride/input.csv
similarity index 100%
rename from test/carelink/bolus/normal/withOverride/input.csv
rename to test/node/carelink/bolus/normal/withOverride/input.csv
diff --git a/test/carelink/bolus/normal/withOverride/output.json b/test/node/carelink/bolus/normal/withOverride/output.json
similarity index 100%
rename from test/carelink/bolus/normal/withOverride/output.json
rename to test/node/carelink/bolus/normal/withOverride/output.json
diff --git a/test/carelink/bolus/normal/withWizard/input.csv b/test/node/carelink/bolus/normal/withWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/normal/withWizard/input.csv
rename to test/node/carelink/bolus/normal/withWizard/input.csv
diff --git a/test/carelink/bolus/normal/withWizard/output.json b/test/node/carelink/bolus/normal/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/normal/withWizard/output.json
rename to test/node/carelink/bolus/normal/withWizard/output.json
diff --git a/test/carelink/bolus/normal/withWizardInterveningBG/input.csv b/test/node/carelink/bolus/normal/withWizardInterveningBG/input.csv
similarity index 100%
rename from test/carelink/bolus/normal/withWizardInterveningBG/input.csv
rename to test/node/carelink/bolus/normal/withWizardInterveningBG/input.csv
diff --git a/test/carelink/bolus/normal/withWizardInterveningBG/output.json b/test/node/carelink/bolus/normal/withWizardInterveningBG/output.json
similarity index 100%
rename from test/carelink/bolus/normal/withWizardInterveningBG/output.json
rename to test/node/carelink/bolus/normal/withWizardInterveningBG/output.json
diff --git a/test/carelink/bolus/square/noWizard/input.csv b/test/node/carelink/bolus/square/noWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/square/noWizard/input.csv
rename to test/node/carelink/bolus/square/noWizard/input.csv
diff --git a/test/carelink/bolus/square/noWizard/output.json b/test/node/carelink/bolus/square/noWizard/output.json
similarity index 100%
rename from test/carelink/bolus/square/noWizard/output.json
rename to test/node/carelink/bolus/square/noWizard/output.json
diff --git a/test/carelink/bolus/square/noWizardInterrupted/input.csv b/test/node/carelink/bolus/square/noWizardInterrupted/input.csv
similarity index 100%
rename from test/carelink/bolus/square/noWizardInterrupted/input.csv
rename to test/node/carelink/bolus/square/noWizardInterrupted/input.csv
diff --git a/test/carelink/bolus/square/noWizardInterrupted/output.json b/test/node/carelink/bolus/square/noWizardInterrupted/output.json
similarity index 100%
rename from test/carelink/bolus/square/noWizardInterrupted/output.json
rename to test/node/carelink/bolus/square/noWizardInterrupted/output.json
diff --git a/test/carelink/bolus/square/withWizard/input.csv b/test/node/carelink/bolus/square/withWizard/input.csv
similarity index 100%
rename from test/carelink/bolus/square/withWizard/input.csv
rename to test/node/carelink/bolus/square/withWizard/input.csv
diff --git a/test/carelink/bolus/square/withWizard/output.json b/test/node/carelink/bolus/square/withWizard/output.json
similarity index 100%
rename from test/carelink/bolus/square/withWizard/output.json
rename to test/node/carelink/bolus/square/withWizard/output.json
diff --git a/test/carelink/cbg/input.csv b/test/node/carelink/cbg/input.csv
similarity index 100%
rename from test/carelink/cbg/input.csv
rename to test/node/carelink/cbg/input.csv
diff --git a/test/carelink/cbg/output.json b/test/node/carelink/cbg/output.json
similarity index 100%
rename from test/carelink/cbg/output.json
rename to test/node/carelink/cbg/output.json
diff --git a/test/carelink/mockSimulator.js b/test/node/carelink/mockSimulator.js
similarity index 100%
rename from test/carelink/mockSimulator.js
rename to test/node/carelink/mockSimulator.js
diff --git a/test/carelink/overlaps/no-overlap.csv b/test/node/carelink/overlaps/no-overlap.csv
similarity index 100%
rename from test/carelink/overlaps/no-overlap.csv
rename to test/node/carelink/overlaps/no-overlap.csv
diff --git a/test/carelink/overlaps/overlap.csv b/test/node/carelink/overlaps/overlap.csv
similarity index 100%
rename from test/carelink/overlaps/overlap.csv
rename to test/node/carelink/overlaps/overlap.csv
diff --git a/test/carelink/settings/noChanges/input.csv b/test/node/carelink/settings/noChanges/input.csv
similarity index 100%
rename from test/carelink/settings/noChanges/input.csv
rename to test/node/carelink/settings/noChanges/input.csv
diff --git a/test/carelink/settings/noChanges/output.json b/test/node/carelink/settings/noChanges/output.json
similarity index 100%
rename from test/carelink/settings/noChanges/output.json
rename to test/node/carelink/settings/noChanges/output.json
diff --git a/test/carelink/settings/withChanges/input.csv b/test/node/carelink/settings/withChanges/input.csv
similarity index 100%
rename from test/carelink/settings/withChanges/input.csv
rename to test/node/carelink/settings/withChanges/input.csv
diff --git a/test/carelink/settings/withChanges/output.json b/test/node/carelink/settings/withChanges/output.json
similarity index 100%
rename from test/carelink/settings/withChanges/output.json
rename to test/node/carelink/settings/withChanges/output.json
diff --git a/test/carelink/settings/withSameTimeChanges/input.csv b/test/node/carelink/settings/withSameTimeChanges/input.csv
similarity index 100%
rename from test/carelink/settings/withSameTimeChanges/input.csv
rename to test/node/carelink/settings/withSameTimeChanges/input.csv
diff --git a/test/carelink/settings/withSameTimeChanges/output.json b/test/node/carelink/settings/withSameTimeChanges/output.json
similarity index 100%
rename from test/carelink/settings/withSameTimeChanges/output.json
rename to test/node/carelink/settings/withSameTimeChanges/output.json
diff --git a/test/carelink/smbg/input.csv b/test/node/carelink/smbg/input.csv
similarity index 100%
rename from test/carelink/smbg/input.csv
rename to test/node/carelink/smbg/input.csv
diff --git a/test/carelink/smbg/output.json b/test/node/carelink/smbg/output.json
similarity index 100%
rename from test/carelink/smbg/output.json
rename to test/node/carelink/smbg/output.json
diff --git a/test/carelink/suspend/input.csv b/test/node/carelink/suspend/input.csv
similarity index 100%
rename from test/carelink/suspend/input.csv
rename to test/node/carelink/suspend/input.csv
diff --git a/test/carelink/suspend/output.json b/test/node/carelink/suspend/output.json
similarity index 100%
rename from test/carelink/suspend/output.json
rename to test/node/carelink/suspend/output.json
diff --git a/test/carelink/testCarelinkSimulator.js b/test/node/carelink/testCarelinkSimulator.js
similarity index 99%
rename from test/carelink/testCarelinkSimulator.js
rename to test/node/carelink/testCarelinkSimulator.js
index d47117c9f7..99d79efb08 100644
--- a/test/carelink/testCarelinkSimulator.js
+++ b/test/node/carelink/testCarelinkSimulator.js
@@ -20,7 +20,7 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var pwdSimulator = require('../../lib/carelink/carelinkSimulator.js');
+var pwdSimulator = require('../../../lib/carelink/carelinkSimulator.js');
function attachPrev(arr) {
var prevBasal = null;
diff --git a/test/carelink/testCommon.js b/test/node/carelink/testCommon.js
similarity index 98%
rename from test/carelink/testCommon.js
rename to test/node/carelink/testCommon.js
index c72ef689cc..0d0d4063e5 100644
--- a/test/carelink/testCommon.js
+++ b/test/node/carelink/testCommon.js
@@ -20,7 +20,7 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var common = require('../../lib/carelink/common');
+var common = require('../../../lib/carelink/common');
describe('common', function() {
diff --git a/test/carelink/testDriver.js b/test/node/carelink/testDriver.js
similarity index 96%
rename from test/carelink/testDriver.js
rename to test/node/carelink/testDriver.js
index adfe62b291..f9beb2bd21 100644
--- a/test/carelink/testDriver.js
+++ b/test/node/carelink/testDriver.js
@@ -16,13 +16,13 @@
*/
/* global describe, it */
-/*jshint quotmark: false */
+/* eslint quotes: 0 */
var async = require('async');
var fs = require('fs');
var expect = require('salinity').expect;
-var carelinkDriver = require('../../lib/drivers/carelinkDriver.js')(require('./mockSimulator.js'));
+var carelinkDriver = require('../../../lib/drivers/carelinkDriver.js')(require('./mockSimulator.js'));
function noop() {}
diff --git a/test/carelink/testRemoveOverlaps.js b/test/node/carelink/testRemoveOverlaps.js
similarity index 97%
rename from test/carelink/testRemoveOverlaps.js
rename to test/node/carelink/testRemoveOverlaps.js
index b46f0da8d8..b8529465c8 100644
--- a/test/carelink/testRemoveOverlaps.js
+++ b/test/node/carelink/testRemoveOverlaps.js
@@ -24,7 +24,7 @@ var util = require('util');
var expect = require('salinity').expect;
-var removeOverlaps = require('../../lib/carelink/removeOverlapping');
+var removeOverlaps = require('../../../lib/carelink/removeOverlapping');
describe('removeOverlapping', function() {
function convertRawValues(e) {
diff --git a/test/carelink/testSerial.js b/test/node/carelink/testSerial.js
similarity index 98%
rename from test/carelink/testSerial.js
rename to test/node/carelink/testSerial.js
index 13bfda0fe4..d3d8e26399 100644
--- a/test/carelink/testSerial.js
+++ b/test/node/carelink/testSerial.js
@@ -20,7 +20,7 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var getDeviceInfo = require('../../lib/carelink/getDeviceInfo');
+var getDeviceInfo = require('../../../lib/carelink/getDeviceInfo');
describe('getDeviceInfo', function() {
var fakeRows = [
diff --git a/test/core/testGetIn.js b/test/node/core/testGetIn.js
similarity index 96%
rename from test/core/testGetIn.js
rename to test/node/core/testGetIn.js
index 3bb9e64e5e..24b75d6d59 100644
--- a/test/core/testGetIn.js
+++ b/test/node/core/testGetIn.js
@@ -19,7 +19,7 @@
var expect = require('salinity').expect;
-var getIn = require('../../lib/core/getIn');
+var getIn = require('../../../lib/core/getIn');
describe('getIn', function() {
var obj = {
diff --git a/test/dexcom/testUserSettingsChanges.js b/test/node/dexcom/testUserSettingsChanges.js
similarity index 98%
rename from test/dexcom/testUserSettingsChanges.js
rename to test/node/dexcom/testUserSettingsChanges.js
index aa4fb369bb..8113eac62b 100644
--- a/test/dexcom/testUserSettingsChanges.js
+++ b/test/node/dexcom/testUserSettingsChanges.js
@@ -21,8 +21,8 @@ var _ = require('lodash');
var expect = require('salinity').expect;
var sundial = require('sundial');
-var builder = require('../../lib/objectBuilder')();
-var userSettingsChanges = require('../../lib/dexcom/userSettingsChanges');
+var builder = require('../../../lib/objectBuilder')();
+var userSettingsChanges = require('../../../lib/dexcom/userSettingsChanges');
describe('userSettingsChanges.js', function() {
var settingsTemplate = {
diff --git a/test/insulet/testInsuletSimulator.js b/test/node/insulet/testInsuletSimulator.js
similarity index 99%
rename from test/insulet/testInsuletSimulator.js
rename to test/node/insulet/testInsuletSimulator.js
index 12e7b96541..f1def129cb 100644
--- a/test/insulet/testInsuletSimulator.js
+++ b/test/node/insulet/testInsuletSimulator.js
@@ -20,8 +20,8 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var pwdSimulator = require('../../lib/insulet/insuletSimulator.js');
-var builder = require('../../lib/objectBuilder')();
+var pwdSimulator = require('../../../lib/insulet/insuletSimulator.js');
+var builder = require('../../../lib/objectBuilder')();
describe('insuletSimulator.js', function() {
var simulator = null;
diff --git a/test/insulet/testLogic.js b/test/node/insulet/testLogic.js
similarity index 97%
rename from test/insulet/testLogic.js
rename to test/node/insulet/testLogic.js
index 74d8135ce6..ce89acd78a 100644
--- a/test/insulet/testLogic.js
+++ b/test/node/insulet/testLogic.js
@@ -20,7 +20,7 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var logic = require('../../lib/insulet/objectBuildingLogic');
+var logic = require('../../../lib/insulet/objectBuildingLogic');
describe('objectBuildingLogic', function() {
describe('calculateNetRecommendation', function() {
diff --git a/test/tandem/testTandemSimulator.js b/test/node/tandem/testTandemSimulator.js
similarity index 99%
rename from test/tandem/testTandemSimulator.js
rename to test/node/tandem/testTandemSimulator.js
index ea6032bc4b..debed4d5a7 100644
--- a/test/tandem/testTandemSimulator.js
+++ b/test/node/tandem/testTandemSimulator.js
@@ -20,8 +20,8 @@
var _ = require('lodash');
var expect = require('salinity').expect;
-var pwdSimulator = require('../../lib/tandem/tandemSimulator.js');
-var builder = require('../../lib/objectBuilder')();
+var pwdSimulator = require('../../../lib/tandem/tandemSimulator.js');
+var builder = require('../../../lib/objectBuilder')();
describe('tandemSimulator.js', function() {
var simulator = null;
diff --git a/test/testCommonSimulations.js b/test/node/testCommonSimulations.js
similarity index 95%
rename from test/testCommonSimulations.js
rename to test/node/testCommonSimulations.js
index d72b206497..df49176675 100644
--- a/test/testCommonSimulations.js
+++ b/test/node/testCommonSimulations.js
@@ -15,14 +15,14 @@
* == BSD2 LICENSE ==
*/
-/* global describe, it */
+/*eslint-env mocha*/
var expect = require('salinity').expect;
-var builder = require('../lib/objectBuilder')();
-var TZOUtil = require('../lib/TimezoneOffsetUtil');
+var builder = require('../../lib/objectBuilder')();
+var TZOUtil = require('../../lib/TimezoneOffsetUtil');
-var common = require('../lib/commonSimulations');
+var common = require('../../lib/commonSimulations');
describe('commonSimulations.js', function(){
diff --git a/test/testDriverManager.js b/test/node/testDriverManager.js
similarity index 92%
rename from test/testDriverManager.js
rename to test/node/testDriverManager.js
index 4d41f9c13b..22da5f6c19 100644
--- a/test/testDriverManager.js
+++ b/test/node/testDriverManager.js
@@ -15,11 +15,11 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var expect = require('salinity').expect;
-var driverManager = require('../lib/driverManager.js');
+var driverManager = require('../../lib/driverManager.js');
describe('driverManager.js', function(){
diff --git a/test/testHidDevice.js b/test/node/testHidDevice.js
similarity index 96%
rename from test/testHidDevice.js
rename to test/node/testHidDevice.js
index 4793fbceb5..afa7fc88f2 100644
--- a/test/testHidDevice.js
+++ b/test/node/testHidDevice.js
@@ -15,16 +15,16 @@
* == BSD2 LICENSE ==
*/
+/*eslint-env mocha*/
+
/*
* IOET
* Test for new hidDevice facilities
* */
-/* global beforeEach, describe, it */
-
var expect = require('salinity').expect;
-var hidDevice = require('../lib/hidDevice.js');
+var hidDevice = require('../../lib/hidDevice.js');
describe('hidDevice.js', function(){
diff --git a/test/testLocalStore.js b/test/node/testLocalStore.js
similarity index 96%
rename from test/testLocalStore.js
rename to test/node/testLocalStore.js
index f7ce6e7470..e888ad4da9 100644
--- a/test/testLocalStore.js
+++ b/test/node/testLocalStore.js
@@ -15,12 +15,12 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var _ = require('lodash');
var expect = require('salinity').expect;
-var localStore = require('../lib/core/localStore');
+var localStore = require('../../lib/core/localStore');
describe('localStore [node.js version for testing]', function() {
it('is an object', function() {
diff --git a/test/testObjectBuilder.js b/test/node/testObjectBuilder.js
similarity index 96%
rename from test/testObjectBuilder.js
rename to test/node/testObjectBuilder.js
index 769f358061..60fdf444cc 100644
--- a/test/testObjectBuilder.js
+++ b/test/node/testObjectBuilder.js
@@ -15,12 +15,12 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var _ = require('lodash');
var expect = require('salinity').expect;
-var ObjectBuilder = require('../lib/objectBuilder.js');
+var objectBuilder = require('../../lib/objectBuilder.js');
describe('objectBuilder.js', function(){
var objBuilder = null;
@@ -29,7 +29,7 @@ describe('objectBuilder.js', function(){
var bob;
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
});
describe('setDefaults', function(){
@@ -51,7 +51,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeBloodKetone'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -77,7 +77,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeCBG'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -103,7 +103,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeCGMSettings'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -136,7 +136,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventAlarm'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -163,7 +163,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventCalibration'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -190,7 +190,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventReservoirChange'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -216,7 +216,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventResume'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -244,7 +244,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventSuspend'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -272,7 +272,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDeviceEventTimeChange'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -298,7 +298,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeDualBolus'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -329,7 +329,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeFood'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -354,7 +354,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeNormalBolus'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -381,7 +381,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeNormalBolus'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -406,7 +406,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makePumpSettings'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -436,7 +436,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeScheduledBasal'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -465,7 +465,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeSMBG'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -492,7 +492,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeSquareBolus'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -522,7 +522,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeSuspendBasal'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -551,7 +551,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeTempBasal'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -582,7 +582,7 @@ describe('objectBuilder.js', function(){
var defaults = {deviceId:'makeWizard'};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
@@ -615,7 +615,7 @@ describe('objectBuilder.js', function(){
var defaults = {};
beforeEach(function(){
- bob = ObjectBuilder();
+ bob = objectBuilder();
bob.setDefaults(defaults);
});
diff --git a/test/testSerialDevice.js b/test/node/testSerialDevice.js
similarity index 97%
rename from test/testSerialDevice.js
rename to test/node/testSerialDevice.js
index 74182f046c..0237e03432 100644
--- a/test/testSerialDevice.js
+++ b/test/node/testSerialDevice.js
@@ -15,11 +15,11 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var expect = require('salinity').expect;
-var serialDevice = require('../lib/serialDevice.js');
+var serialDevice = require('../../lib/serialDevice.js');
/*
setPattern: setPattern,
diff --git a/test/testStatusManager.js b/test/node/testStatusManager.js
similarity index 91%
rename from test/testStatusManager.js
rename to test/node/testStatusManager.js
index baea675793..3546ea45e6 100644
--- a/test/testStatusManager.js
+++ b/test/node/testStatusManager.js
@@ -15,11 +15,11 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var expect = require('salinity').expect;
-var statusManager = require('../lib/statusManager.js');
+var statusManager = require('../../lib/statusManager.js');
describe('statusManager.js', function(){
diff --git a/test/testStruct.js b/test/node/testStruct.js
similarity index 99%
rename from test/testStruct.js
rename to test/node/testStruct.js
index 1312c39319..dc01866b54 100644
--- a/test/testStruct.js
+++ b/test/node/testStruct.js
@@ -15,11 +15,11 @@
* == BSD2 LICENSE ==
*/
-/* global beforeEach, describe, it */
+/*eslint-env mocha*/
var expect = require('salinity').expect;
-var struct = require('../lib/struct.js');
+var struct = require('../../lib/struct.js');
describe('struct.js', function(){
diff --git a/test/testTimezoneOffsetUtil.js b/test/node/testTimezoneOffsetUtil.js
similarity index 99%
rename from test/testTimezoneOffsetUtil.js
rename to test/node/testTimezoneOffsetUtil.js
index 50cb1c7183..3f1309205e 100644
--- a/test/testTimezoneOffsetUtil.js
+++ b/test/node/testTimezoneOffsetUtil.js
@@ -15,14 +15,14 @@
* == BSD2 LICENSE ==
*/
-/* global describe, it */
+/*eslint-env mocha*/
var _ = require('lodash');
var d3 = require('d3');
var expect = require('salinity').expect;
-var builder = require('../lib/objectBuilder')();
-var TZOUtil = require('../lib/TimezoneOffsetUtil');
+var builder = require('../../lib/objectBuilder')();
+var TZOUtil = require('../../lib/TimezoneOffsetUtil');
describe('TimezoneOffsetUtil.js', function(){
it('exports a function', function(){
diff --git a/test/ui/testAppActions.js b/test/ui/testAppActions.js
deleted file mode 100644
index 54ffcb4480..0000000000
--- a/test/ui/testAppActions.js
+++ /dev/null
@@ -1,979 +0,0 @@
-/*
-* == BSD2 LICENSE ==
-* Copyright (c) 2014, 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
-* License as published by the Open Source Initiative at opensource.org.
-*
-* This program is distributed in the hope that it will be useful, but WITHOUT
-* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-* FOR A PARTICULAR PURPOSE. See the License for more details.
-*
-* You should have received a copy of the License along with this program; if
-* not, you can obtain one from Tidepool Project at tidepool.org.
-* == BSD2 LICENSE ==
-*/
-
-var _ = require('lodash');
-var proxyquire = require('proxyquire').noCallThru();
-var expect = require('salinity').expect;
-var appState = require('../../lib/state/appState');
-
-
-describe('appActions', function() {
- // Mock all I/O
- var config, now, sundial, localStore, api, jellyfish, device, carelink;
- var app;
- var appActions;
-
- var defaultUploadGroups = [{
- profile: {
- fullName: 'Bob',
- patient: {
- birthday: '2000-01-01',
- diagnosisDate: '2010-04-01',
- about: ''
- }
- },
- userid: '11'
- }, {
- profile: {
- fullName: 'Alice',
- patient: {
- birthday: '1985-07-04',
- diagnosisDate: '1993-03-25',
- about: 'Foo bar'
- }
- },
- userid: '12'
- }];
-
- beforeEach(function() {
-
- config = {};
- now = '2014-01-31T22:00:00-05:00';
- sundial = {
- utcDateString: function() { return now; }
- };
- localStore = require('../../lib/core/localStore')({
- devices: {'11': [{
- key: 'carelink',
- timezone: 'oldTz'
- }]}
- });
- api = {};
-
- jellyfish = {};
- device = {};
- carelink = {};
-
- app = {
- state: {},
- setState: function(updates) {
- this.state = _.assign(this.state, updates);
- }
- };
- appState.bindApp(app);
-
- appActions = proxyquire('../../lib/state/appActions', {
- '../config': config,
- 'sundial': sundial,
- '../core/localStore': localStore,
- '../core/api': api,
- '../jellyfishClient': function() { return jellyfish; },
- '../core/device': device,
- '../core/carelink': carelink
- });
- appActions.bindApp(app);
- });
-
- it('binds to app component', function() {
- app.state.FOO = 'bar';
- expect(appActions.app.state.FOO).to.equal('bar');
- });
-
- describe('load', function() {
- beforeEach(function() {
- api.init = function(options, cb) { cb(); };
- device.init = function(options, cb) { cb(); };
- carelink.init = function(options, cb) { cb(); };
-
- api.user = {};
- api.user.account = function(cb) { cb(); };
- api.user.profile = function(cb) { cb(); };
- api.user.getUploadGroups = function(cb) { cb(null, defaultUploadGroups); };
- api.setHosts = function() {};
- });
-
- it('initializes all I/O services', function(done) {
- var initialized = {};
- var mark = function(name, cb) {
- initialized[name] = true;
- cb();
- };
- localStore.init = function(options, cb) { mark('localStore', cb); };
- api.init = function(options, cb) { mark('api', cb); };
- device.init = function(options, cb) { mark('device', cb); };
- carelink.init = function(options, cb) { mark('carelink', cb); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(initialized.localStore).to.be.true;
- expect(initialized.api).to.be.true;
- expect(initialized.device).to.be.true;
- expect(initialized.carelink).to.be.true;
- done();
- });
- });
-
- it('goes to login page if no session found', function(done) {
- localStore.getInitialState = function() {};
- localStore.init = function(options, cb) { cb(); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.page).to.equal('login');
- done();
- });
- });
-
- it('goes to main page if local session found and targeted devices fetched from localStore', function(done) {
- api.init = function(options, cb) { cb(null, {token: '1234'}); };
- api.user.account = function(cb) { cb(null, {userid: '11'}); };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.page).to.equal('main');
- done();
- });
- });
-
- it('goes to settings page if local session found and no targeted devices fetched from localStore', function(done) {
- api.init = function(options, cb) { cb(null, {token: '1234'}); };
- api.user.account = function(cb) { cb(null, {userid: '12'}); };
- api.user.profile = function(cb) { cb(null, {fullName: 'Alice'}); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.page).to.equal('settings');
- done();
- });
- });
-
- it('loads logged-in user if local session found', function(done) {
- api.init = function(options, cb) { cb(null, {token: '1234'}); };
- api.user.account = function(cb) { cb(null, {userid: '11'}); };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob'}); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.user).to.deep.equal({
- userid: '11',
- profile: {fullName: 'Bob'},
- uploadGroups: defaultUploadGroups
- });
- done();
- });
- });
-
- it('sets target user id as logged-in userid if data storage exists for logged-in user', function(done) {
- api.init = function(options, cb) { cb(null, {token: '1234'}); };
- api.user.account = function(cb) { cb(null, {userid: '11'}); };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.targetId).to.equal('11');
- done();
- });
- });
-
- it('sets target user id to other userid if data storage does not exist for logged-in user', function(done) {
- api.init = function(options, cb) { cb(null, {token: '1234'}); };
- api.user.account = function(cb) { cb(null, {userid: '2'}); };
- api.user.profile = function(cb) { cb(null, {fullName: 'Cookie'}); };
- api.user.getUploadGroups = function(cb) { cb(null, [defaultUploadGroups[1], {
- profile: {
- fullName: 'Cookie'
- },
- userid: '2'
- }]); };
-
- appActions.load(function(err) {
- if (err) throw err;
- expect(app.state.targetId).to.equal('12');
- done();
- });
- });
-
- });
-
- describe('login', function() {
- var loginMetricsCall = {};
-
- beforeEach(function() {
- api.user = {};
- api.user.login = function(credentials, options, cb) { cb(); };
- api.user.profile = function(cb) { cb(); };
- api.user.getUploadGroups = function(cb) { cb(null, defaultUploadGroups); };
- api.metrics = { track : function(one, two) { loginMetricsCall.one = one; loginMetricsCall.two = two; }};
- });
-
- it('goes to settings page by default', function(done) {
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
- expect(app.state.page).to.equal('settings');
- expect(loginMetricsCall).to.not.be.empty;
- expect(loginMetricsCall.one).to.equal(appActions.trackedState.LOGIN_SUCCESS);
- done();
- });
- });
-
- it('goes to main page if login successful and targeted devices fetched from localStore', function(done) {
- api.user.login = function(credentials, options, cb) {
- cb(null, {user: {userid: '11'}});
- };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
-
- expect(app.state.page).to.equal('main');
- done();
- });
- });
-
- it('goes to settings page if login successful and targeted devices not fetched from localStore', function(done) {
- api.user.login = function(credentials, options, cb) {
- cb(null, {user: {userid: '12'}});
- };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
-
- expect(app.state.page).to.equal('settings');
- done();
- });
- });
-
- it('loads logged-in user if login successful', function(done) {
- api.user.login = function(credentials, options, cb) {
- cb(null, {user: {userid: '11'}});
- };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob'}); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
-
- expect(app.state.user).to.deep.equal({
- userid: '11',
- profile: {fullName: 'Bob'},
- uploadGroups: defaultUploadGroups
- });
- done();
- });
- });
-
- it('calls callback with error if login failed', function(done) {
-
- var loginError = {message: 'login failed', step: 'platform_login'};
-
- api.user.login = function(credentials, options, cb) {
- cb(loginError);
- };
-
- appActions.login({}, {}, function(err) {
- expect(err.message).to.contain(loginError.message);
- done();
- });
- });
-
- it('sets target user id as logged-in user id if data storage exists for logged-in user', function(done) {
- api.user.login = function(credentials, options, cb) {
- cb(null, {user: {userid: '11'}});
- };
- api.user.profile = function(cb) { cb(null, {fullName: 'Bob', patient: {about: 'Foo'}}); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
- expect(app.state.targetId).to.equal('11');
- done();
- });
- });
-
- it('sets target user id to other userid if data storage does not exist for logged-in user', function(done) {
- api.user.login = function(credentials, options, cb) {
- cb(null, {user: {userid: '2'}});
- };
- api.user.getUploadGroups = function(cb) { cb(null, [defaultUploadGroups[1], {
- profile: {
- fullName: 'Cookie'
- },
- userid: '2'
- }]); };
-
- appActions.login({}, {}, function(err) {
- if (err) throw err;
- expect(app.state.targetId).to.equal('12');
- done();
- });
- });
-
- });
-
- describe('viewData', function() {
-
- var viewDataMetricsCall = {};
-
- beforeEach(function() {
- api.metrics = { track : function(one, two) { viewDataMetricsCall.one = one; viewDataMetricsCall.two = two; }};
- });
-
- it('logs metric', function(done) {
- appActions.viewData();
- expect(viewDataMetricsCall).to.not.be.empty;
- expect(viewDataMetricsCall.one).to.equal(appActions.trackedState.SEE_IN_BLIP);
- done();
- });
- });
-
- describe('logout', function() {
-
- var logoutMetricsCall = {};
-
- beforeEach(function() {
- api.user = {};
- api.user.logout = function(cb) { cb(); };
- api.metrics = { track : function(one, two) { logoutMetricsCall.one = one; logoutMetricsCall.two = two; }};
- });
-
- it('resets app state', function(done) {
- var uploads = [1, 2, 3];
- app.state = {
- user: {userid: '11'},
- targetId: '11',
- uploads: uploads
- };
- appActions.logout(function(err) {
- if (err) throw err;
- expect(app.state.user).to.not.exist;
- expect(app.state.targetId).to.not.exist;
- expect(app.state.uploads).to.not.equal(uploads);
- expect(logoutMetricsCall).to.not.be.empty;
- expect(logoutMetricsCall.one).to.equal(appActions.trackedState.LOGOUT_CLICKED);
- done();
- });
- });
-
- it('goes back to login page', function(done) {
- app.state.page = 'main';
- appActions.logout(function(err) {
- if (err) throw err;
- expect(app.state.page).to.equal('login');
- done();
- });
- });
-
- });
-
- describe('detectDevices', function() {
- var connectedDevices;
-
- beforeEach(function() {
- connectedDevices = [];
- device.detectAll = function(cb) {
- return cb(null, connectedDevices);
- };
- });
-
- it('adds a new device upload', function(done) {
-
- app.state.uploads = [];
- connectedDevices = [{
- driverId: 'Dexcom',
- usb: 3
- }];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source).to.deep.equal({
- type: 'device',
- driverId: 'Dexcom',
- usb: 3,
- connected: true
- });
- done();
- });
- });
-
- it('keeps carelink at the beginning when adding new device', function(done) {
- app.state.uploads = [
- {source: {type: 'carelink'}}
- ];
- connectedDevices = [{
- driverId: 'Dexcom',
- usb: 3
- }];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(2);
- expect(app.state.uploads[0].source.type).to.equal('carelink');
- done();
- });
- });
-
- it('marks device upload as disconnected', function(done) {
- app.state.uploads = [
- {
- source: {
- type: 'device',
- driverId: 'Dexcom',
- usb: 3,
- connected: true
- }
- }
- ];
- connectedDevices = [];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source.connected).to.be.false;
- done();
- });
- });
-
- it('resets progress for disconnected device', function(done) {
- app.state.uploads = [
- {
- source: {
- type: 'device',
- driverId: 'Dexcom',
- usb: 3,
- connected: true
- },
- progress: {}
- }
- ];
- connectedDevices = [];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source.progress).to.not.exist;
- done();
- });
- });
-
- it('marks device upload as connected', function(done) {
- app.state.uploads = [
- {
- source: {
- type: 'device',
- driverId: 'Dexcom',
- usb: 3,
- connected: false
- }
- }
- ];
- connectedDevices = [{
- driverId: 'Dexcom',
- usb: 3
- }];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source.connected).to.be.true;
- done();
- });
- });
-
- it('updates connected device info', function(done) {
- app.state.uploads = [
- {
- source: {
- type: 'device',
- driverId: 'Dexcom',
- usb: 3,
- connected: true
- }
- }
- ];
- connectedDevices = [{
- driverId: 'Dexcom',
- usb: 11
- }];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source.usb).to.equal(11);
- done();
- });
- });
-
- it('keeps one upload per device driverId', function(done) {
- app.state.uploads = [
- {
- source: {
- type: 'device',
- driverId: 'Dexcom',
- serialNumber: 'AA11',
- usb: 3,
- connected: true
- }
- }
- ];
- connectedDevices = [{
- driverId: 'Dexcom',
- serialNumber: 'BB22',
- usb: 11
- }];
-
- appActions.detectDevices(function(err) {
- if (err) throw err;
- expect(app.state.uploads).to.have.length(1);
- expect(app.state.uploads[0].source.serialNumber).to.equal('BB22');
- done();
- });
- });
-
- });
-
- describe('hideUnavailableDevices', function() {
- beforeEach(function() {
- var state = appState.getInitial();
- app.setState(state);
- });
-
- describe('[windows]', function() {
- beforeEach(function() {
- app.setState({_os: 'win'});
- });
-
- it('excludes nothing', function() {
- expect(app.state.uploads.length).to.equal(10);
- appActions._hideUnavailableDevices();
- expect(app.state.uploads.length).to.equal(10);
- });
- });
-
- describe('[mac]', function() {
- beforeEach(function() {
- app.setState({_os: 'mac'});
- });
-
- it('excludes all Abbott devices', function() {
- expect(app.state.uploads.length).to.equal(10);
- appActions._hideUnavailableDevices();
- expect(app.state.uploads.length).to.equal(7);
- expect(_.findWhere(app.state.uploads, {key: 'precisionxtra'})).to.not.be.ok;
- expect(_.findWhere(app.state.uploads, {key: 'abbottfreestylelite'})).to.not.be.ok;
- expect(_.findWhere(app.state.uploads, {key: 'abbottfreestylefreedomlite'})).to.not.be.ok;
- });
- });
- });
-
- describe('chooseDevices', function() {
- beforeEach(function() {
- app.state = {
- dropMenu: true,
- page: 'main'
- };
- });
-
- it('redirects to settings page and clears dropMenu', function() {
- appActions.chooseDevices();
- expect(app.state.dropMenu).to.be.false;
- expect(app.state.page).to.equal('settings');
- });
- });
-
- describe('addOrRemoveTargetDevice', function() {
- beforeEach(function() {
- app.state = {
- targetDevices: []
- };
- });
-
- it('adds the device if the event target is checked', function() {
- appActions.addOrRemoveTargetDevice({target: {value: 'foo', checked: true}});
- expect(app.state.targetDevices).to.deep.equal(['foo']);
- });
-
- it('removes the device if the event target is not checked', function() {
- app.state.targetDevices = ['foo', 'Kiwi'];
- appActions.addOrRemoveTargetDevice({target: {value: 'foo', checked: false}});
- appActions.addOrRemoveTargetDevice({target: {value: 'bar', checked: false}});
- expect(app.state.targetDevices).to.deep.equal(['Kiwi']);
- });
- });
-
- describe('storeUserTargets', function() {
- beforeEach(function() {
- app.state = {
- page: 'settings',
- targetDevices: ['foo', 'bar'],
- targetTimezone: 'fooTz'
- };
- });
-
- it('saves the current targetDevices in the app state in the localStore under the target userid', function() {
- expect(localStore.getItem('devices')['11'][0].key).to.deep.equal('carelink');
- appActions.storeUserTargets('11');
- expect(_.pluck(localStore.getItem('devices')['11'], 'key')).to.deep.equal(['foo', 'bar']);
- });
-
- it('saves the current targetTimezone in the app state in the localStore along with each target device', function() {
- expect(localStore.getItem('devices')['11'][0].timezone).to.equal('oldTz');
- appActions.storeUserTargets('11');
- expect(_.uniq(_.pluck(localStore.getItem('devices')['11'], 'timezone'))[0]).to.equal('fooTz');
- });
-
- it('also redirects to main page', function() {
- expect(app.state.page).to.equal('settings');
- appActions.storeUserTargets('11');
- expect(app.state.page).to.equal('main');
- });
- });
-
- describe('storeUserTargets, timezone empty', function() {
- beforeEach(function() {
- app.state = {
- page: 'settings',
- targetDevices: ['foo', 'bar'],
- targetTimezone: ''
- };
- });
-
- it('does not redirect to the main page', function() {
- expect(app.state.page).to.equal('settings');
- appActions.storeUserTargets('11');
- expect(app.state.page).to.equal('settings');
- });
- });
-
- describe('readFile', function() {
- beforeEach(function() {
- app.state = {
- uploads: [{key: 'foo'}]
- };
- });
-
- it('should return an error if the filename doesn\'t end in the specified extension', function() {
- var err = appActions.readFile(0, '11', {name: 'foo.bar'}, '.txt');
- expect(err.message).to.equal(appActions.errorText.E_WRONG_FILE_EXT+'.txt');
- });
- });
-
- describe('uploadDevice', function() {
- var uploadDeviceMetricsCall = {};
- var uploadErrorCall = {};
-
- beforeEach(function() {
- api.metrics = { track : function(one, two) { uploadDeviceMetricsCall.one = one; uploadDeviceMetricsCall.two = two; }};
- api.errors = { log : function(one, two, three) { uploadErrorCall.one = one; uploadErrorCall.two = two; uploadErrorCall.three = three; }};
- });
-
- it('throws an error if upload index is invalid', function() {
- app.state.uploads = [];
-
- expect(appActions.upload.bind(appActions, 0))
- .to.throw(appActions.errorText.E_INVAILD_UPLOAD_INDEX);
- });
-
- it('throws an error if an upload is already in progress', function() {
- app.state.uploads = [{
- progress: {}
- }];
-
- expect(appActions.upload.bind(appActions, 0))
- .to.throw(appActions.errorText.E_UPLOAD_IN_PROGRESS);
- });
-
- it('starts upload with correct progress data', function() {
- now = '2014-01-31T22:00:00-05:00';
- device.detect = _.noop;
- device.upload = _.noop;
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
- appActions.upload(0, {}, _.noop);
- expect(app.state.uploads[0].progress).to.deep.equal({
- targetId: '11',
- start: '2014-01-31T22:00:00-05:00',
- step: 'start',
- percentage: 0
- });
- expect(uploadDeviceMetricsCall).to.not.be.empty;
- expect(uploadDeviceMetricsCall.one).to.equal(appActions.trackedState.UPLOAD_STARTED+' Dexcom');
- });
-
- it('updates upload with correct progress data', function(done) {
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) {
- options.progress('foo', 50);
- expect(app.state.uploads[0].progress).to.have.property('step', 'foo');
- expect(app.state.uploads[0].progress).to.have.property('percentage', 50);
- return cb();
- };
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
-
- appActions.upload(0, {}, done);
- });
-
- it('adds correct object to upload history when complete', function(done) {
- now = '2014-01-31T22:00:00-05:00';
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) {
- now = '2014-01-31T22:00:30-05:00';
- options.progress('cleanup', 100);
- var records = [{}, {}];
- return cb(null, records);
- };
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
-
- appActions.upload(0, {}, function(err) {
- if (err) throw err;
- var instance = {
- targetId: '11',
- start: '2014-01-31T22:00:00-05:00',
- finish: '2014-01-31T22:00:30-05:00',
- step: 'cleanup',
- percentage: 100,
- success: true,
- count: 2
- };
- expect(app.state.uploads[0].progress).to.deep.equal(instance);
- expect(app.state.uploads[0].history).to.have.length(1);
- expect(app.state.uploads[0].history[0]).to.deep.equal(instance);
- expect(uploadDeviceMetricsCall).to.not.be.empty;
- expect(uploadDeviceMetricsCall.one).to.equal(appActions.trackedState.UPLOAD_SUCCESS+' Dexcom');
- done();
- });
- });
-
- it('adds correct object to upload history when upload failed', function(done) {
- now = '2014-01-31T22:00:00-05:00';
- var uploadError = new Error('oops');
- uploadError.step = 'fetching_carelink';
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) {
- now = '2014-01-31T22:00:30-05:00';
- options.progress('fetchData', 50);
- return cb(uploadError);
- };
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
-
- appActions.upload(0, {}, function() {
-
- function checkInstance(actual, expected){
- expect(actual.targetId).to.equal(expected.targetId);
- expect(actual.start).to.equal(expected.start);
- expect(actual.percentage).to.equal(expected.percentage);
- expect(actual.error.name).to.equal('Error');
- }
-
- var instance = {
- targetId: '11',
- start: '2014-01-31T22:00:00-05:00',
- finish: '2014-01-31T22:00:30-05:00',
- step: 'fetchData',
- percentage: 50,
- error: uploadError
- };
-
- expect(app.state.uploads[0].history).to.have.length(1);
- checkInstance(app.state.uploads[0].progress,instance);
- checkInstance(app.state.uploads[0].history[0],instance);
- expect(uploadErrorCall).to.not.be.empty;
- expect(uploadDeviceMetricsCall).to.not.be.empty;
- expect(uploadErrorCall.two).to.equal(appActions.trackedState.UPLOAD_FAILED+' Dexcom');
- expect(uploadDeviceMetricsCall.one).to.equal(appActions.trackedState.UPLOAD_FAILED+' Dexcom');
- done();
- });
- });
-
- it('adds to upload history most recent first', function(done) {
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) { return cb(null, []); };
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- },
- history: [
- {targetId: '1'}
- ]
- }];
- app.state.targetId = '2';
-
- appActions.upload(0, {}, function(err) {
- if (err) throw err;
- expect(app.state.uploads[0].history).to.have.length(2);
- expect(app.state.uploads[0].history[0].targetId).to.equal('2');
- expect(app.state.uploads[0].history[1].targetId).to.equal('1');
- done();
- });
- });
-
- });
-
- describe('reset', function() {
-
- it('throws an error if upload index is invalid', function() {
- app.state.uploads = [];
-
- expect(appActions.reset.bind(appActions, 0))
- .to.throw(appActions.errorText.E_INVAILD_UPLOAD_INDEX);
- });
-
- it('clears upload progress', function() {
- app.state.uploads = [
- {progress: {}}
- ];
-
- appActions.reset(0);
- expect(app.state.uploads[0].progress).to.not.exists;
- });
-
- });
-
- describe('changeGroup', function() {
-
- it('updates user id for uploading', function() {
- app.state.targetId = 'foo';
- appActions.changeGroup('bar');
- expect(app.state.targetId).to.equal('bar');
- });
-
- });
-
- describe('changeTimezone', function() {
-
- it('updates the timezone ', function() {
- app.state.targetTimezone = 'foo';
- appActions.changeTimezone('bar');
- expect(app.state.targetTimezone).to.equal('bar');
- });
-
- });
-
- describe('hideDropMenu', function() {
-
- it('sets the boolean for the dropdown menu to false, always', function() {
- app.state.dropMenu = true;
- appActions.hideDropMenu();
- expect(app.state.dropMenu).to.be.false;
- appActions.hideDropMenu();
- expect(app.state.dropMenu).to.be.false;
- });
-
- });
-
- describe('toggleDropMenu', function() {
-
- it('toggles the boolean for the dropdown menu', function() {
- app.state.dropMenu = true;
- appActions.toggleDropMenu();
- expect(app.state.dropMenu).to.be.false;
- appActions.toggleDropMenu();
- expect(app.state.dropMenu).to.be.true;
- });
-
- });
-
- describe('_handleUploadError', function(){
-
- var uploadDeviceMetricsCall = {};
- var uploadErrorCall = {};
-
- beforeEach(function() {
- api.metrics = { track : function(one, two) { uploadDeviceMetricsCall.one = one; uploadDeviceMetricsCall.two = two; }};
- api.errors = { log : function(one, two, three) { uploadErrorCall.one = one; uploadErrorCall.two = two; uploadErrorCall.three = three; }};
- });
-
- it('each error has a detailed `debug` string attached for logging', function(done) {
- now = '2014-01-31T22:00:00-05:00';
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) {
- now = '2014-01-31T22:00:30-05:00';
- options.progress('fetchData', 50);
- var err = new Error('Oops, we got an error');
- return cb(err);
- };
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
-
- appActions.upload(0, {}, function(err) {
- expect(err.debug).to.contain('Detail: ');
- expect(err.debug).to.contain('Error UTC Time: ');
- expect(err.debug).to.contain('Code: E_');
- expect(err.debug).to.contain('Error Type: Error');
- expect(err.debug).to.contain('Version: tidepool-uploader');
- done();
- });
- });
-
- it('redirects to the `error` page if jellyfish errors because uploader is out-of-date', function(done) {
- now = '2014-01-31T22:00:00-05:00';
- device.detect = function(driverId, options, cb) { return cb(null, {}); };
- device.upload = function(driverId, options, cb) {
- now = '2014-01-31T22:00:30-05:00';
- options.progress('fetchData', 50);
- var err = new Error('Oops, we got an error');
- err.code = 'outdatedVersion';
- return cb(err);
- };
- app.state.targetId = '11';
- app.state.uploads = [{
- source: {
- type: 'device',
- driverId: 'Dexcom'
- }
- }];
-
- appActions.upload(0, {}, function(err) {
- expect(app.state.page).to.equal('error');
- done();
- });
- });
-
- });
-
-});
diff --git a/test/ui/testAppState.js b/test/ui/testAppState.js
deleted file mode 100644
index e9ef9e5e81..0000000000
--- a/test/ui/testAppState.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
-* == BSD2 LICENSE ==
-* Copyright (c) 2014, 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
-* License as published by the Open Source Initiative at opensource.org.
-*
-* This program is distributed in the hope that it will be useful, but WITHOUT
-* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-* FOR A PARTICULAR PURPOSE. See the License for more details.
-*
-* You should have received a copy of the License along with this program; if
-* not, you can obtain one from Tidepool Project at tidepool.org.
-* == BSD2 LICENSE ==
-*/
-
-var _ = require('lodash');
-var proxyquire = require('proxyquire').noCallThru();
-var expect = require('salinity').expect;
-
-describe('appState', function() {
- var config;
- var app;
- var appState;
- beforeEach(function() {
- config = {};
- app = {
- state: {},
- setState: function(updates) {
- this.state = _.assign(this.state, updates);
- }
- };
-
- appState = proxyquire('../../lib/state/appState', {
- '../config': config
- });
- appState.bindApp(app);
- });
-
- it('binds to app component', function() {
- app.state.FOO = 'bar';
- expect(appState.app.state.FOO).to.equal('bar');
- });
-
- describe('isLoggedIn', function() {
-
- it('returns true if there is a logged-in user object', function() {
- app.state.user = {userid: '11'};
-
- expect(appState.isLoggedIn()).to.be.true;
- });
-
- it('returns false if no logged-in user object', function() {
- app.state.user = null;
-
- expect(appState.isLoggedIn()).to.not.be.true;
- });
-
- });
-
- describe('currentUploadIndex', function() {
-
- it('returns index of upload in progress', function() {
- app.state.uploads = [
- {},
- {progress: {}}
- ];
-
- expect(appState.currentUploadIndex()).to.equal(1);
- });
-
- it('returns -1 if no upload in progress', function() {
- app.state.uploads = [
- {}
- ];
-
- expect(appState.currentUploadIndex()).to.equal(-1);
- });
-
- it('returns -1 if upload is complete', function() {
- app.state.uploads = [
- {progress: {finish: '2014-01-31T12:00:00Z'}}
- ];
-
- expect(appState.currentUploadIndex()).to.equal(-1);
- });
-
- });
-
- describe('hasUploadInProgress', function() {
-
- it('returns true if there is an upload in progress', function() {
- app.state.uploads = [
- {},
- {progress: {}}
- ];
-
- expect(appState.hasUploadInProgress()).to.be.true;
- });
-
- it('returns false if no upload in progress', function() {
- app.state.uploads = [];
-
- expect(appState.hasUploadInProgress()).to.not.be.true;
- });
-
- });
-
- describe('deviceCount', function() {
-
- it('returns number of uploads coming from a device', function() {
- app.state.uploads = [
- {source: {type: 'device'}},
- {source: {type: 'carelink'}}
- ];
-
- expect(appState.deviceCount()).to.equal(1);
- });
-
- });
-
- describe('uploadsWithFlags', function() {
-
- beforeEach(function() {
- app.state.targetDevices = ['foo', 'bar', 'balderdash', 'Kiwi'];
- });
-
- it('only includes uploads with keys that are in the current user\'s targeted uploads', function() {
- app.state.uploads = [
- {key: 'foo'},
- {key: 'me'}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(1);
- });
-
- it('adds disabled flag to all uploads not in progress if one is in progress', function() {
- app.state.uploads = [
- {key: 'whatevs'},
- {key: 'foo'},
- {key: 'bar', progress: {}}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].disabled).to.be.true;
- expect(uploads[1].disabled).to.be.not.ok;
- });
-
- it('adds disabled and disconnected flags to disconnected devices', function() {
- app.state.uploads = [
- {key: 'foo', source: {type: 'device', connected: false}},
- {key: 'bar', source: {type: 'device', connected: true}, progress: {}}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].disabled).to.be.ok;
- expect(uploads[0].disconnected).to.be.ok;
- expect(uploads[1].disabled).to.not.be.ok;
- expect(uploads[1].disconnected).to.not.be.ok;
- });
-
- it('adds carelink flag to carelink uploads', function() {
- app.state.uploads = [
- {key: 'foo', source: {type: 'carelink'}},
- {key: 'bar', source: {type: 'device'}}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].carelink).to.be.ok;
- expect(uploads[1].carelink).to.not.be.ok;
- });
-
- it('adds uploading flag to uploads in progress', function() {
- app.state.uploads = [
- {key: 'foo', progress: {}},
- {key: 'bar', progress: {finish: '2014-01-31T12:00:00Z'}},
- {key: 'balderdash'}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(3);
- expect(uploads[0].uploading).to.be.ok;
- expect(uploads[1].uploading).to.not.be.ok;
- expect(uploads[2].uploading).to.not.be.ok;
- });
-
- it('adds fetchingCarelinkData flag to carelink upload just starting', function() {
- app.state.uploads = [
- {key: 'foo', source: {type: 'carelink'}, progress: {step: 'start'}},
- {key: 'bar', source: {type: 'device'}, progress: {step: 'start'}},
- {key: 'balderdash', source: {type: 'carelink'}, progress: {step: 'start', finish: '2014-01-31T12:00:00Z'}},
- {key: 'Kiwi', source: {type: 'carelink'}, progress: {step: 'upload'}}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(4);
- expect(uploads[0].fetchingCarelinkData).to.be.ok;
- expect(uploads[1].fetchingCarelinkData).to.not.be.ok;
- expect(uploads[2].fetchingCarelinkData).to.not.be.ok;
- expect(uploads[3].fetchingCarelinkData).to.not.be.ok;
- });
-
- it('adds completed flag if current instance completed', function() {
- app.state.uploads = [
- {key: 'foo', progress: {finish: '2014-01-31T12:00:00Z'}},
- {key: 'bar'}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].completed).to.be.ok;
- expect(uploads[1].completed).to.not.be.ok;
- });
-
- it('adds successful flag if current instance successful', function() {
- app.state.uploads = [
- {key: 'foo', progress: {finish: '2014-01-31T12:00:00Z', success: true}},
- {key: 'bar'}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].successful).to.be.ok;
- expect(uploads[1].successful).to.not.be.ok;
- });
-
- it('adds failed flag if current instance failed', function() {
- app.state.uploads = [
- {key: 'foo', progress: {finish: '2014-01-31T12:00:00Z', error: 'oops'}},
- {key: 'bar'}
- ];
-
- var uploads = appState.uploadsWithFlags();
- expect(uploads).to.have.length(2);
- expect(uploads[0].failed).to.be.ok;
- expect(uploads[1].failed).to.not.be.ok;
- });
-
- });
-
-});
diff --git a/webpack.config.js b/webpack.config.js
index ac0dbf5a12..37e2f65813 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,12 @@ var _ = require('lodash');
var webpack = require('webpack');
var definePlugin = new webpack.DefinePlugin({
- __DEBUG__: JSON.stringify(JSON.parse(process.env.DEBUG_ERROR || 'false'))
+ // this first as advised to get the correct production build of redux
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) || '"development"',
+ __DEBUG__: JSON.stringify(JSON.parse(process.env.DEBUG_ERROR || 'false')),
+ __REDUX_LOG__: JSON.stringify(JSON.parse(process.env.REDUX_LOG || 'false')),
+ __REDUX_DEV_UI__: JSON.stringify(JSON.parse(process.env.REDUX_DEV_UI || 'false')),
+ __TEST__: false
});
if (process.env.DEBUG_ERROR === 'true') {
@@ -31,19 +36,14 @@ var config = {
},
module: {
loaders: [
- { test: /\.jsx$/, loader: 'jsx' },
+ { test: /\.js$/, exclude: /(node_modules)/, loader: 'babel-loader' },
+ { test: /\.jsx$/, exclude: /(node_modules)/, loader: 'babel-loader' },
{ test: /\.less$/, loader: 'style!css!less' },
{ test: /\.json$/, loader: 'json' }
]
},
plugins: [
- definePlugin,
- new webpack.DefinePlugin({
- 'process.env': Object.keys(process.env).reduce(function(o, k) {
- o[k] = JSON.stringify(process.env[k]);
- return o;
- }, {})
- })
+ definePlugin
],
// to fix the 'broken by design' issue with npm link-ing modules
resolve: { fallback: path.join(__dirname, 'node_modules') },