Blip is a web app for Type-1 Diabetes (T1D) built on top of the Tidepool platform. It allows patients and their "care team" (family, doctors) to visualize their device data and message each other.
Tech stack:
Table of contents:
Requirements:
Clone this repo then install dependencies:
$ npm install
$ bower install
Start the development server (in "mock mode") with:
$ export MOCK=true
$ node develop
Open your web browser and navigate to http://localhost:3000/
.
Configuration values (such as API keys) are set with environment variables (see config/sample.sh
).
You can set environment variables manually, or use a bash script. For example:
source config/dev.sh
Ask the project owners to provide you with config scripts for different environments, or you can create one of your own. It is recommended to put them in the config/
directory, where they will be ignored by Git.
The following snippets of documentation should help you find your way around and contribute to the app's code.
- App (
app/app.js
): Expose a globalwindow.app
object where everything else is attached; create the main React componentapp.component
- Router (
app/router.js
): Handle client-side URI routing (using director); attached to the globalapp
object - Core (
app/core
): Scripts and styles shared by all app components - Components (
app/components
): Reusable React components, the building-blocks of the application - Pages (
app/pages
): Higher-level React components that combine reusable components together; switch from page to page on route change - Services (
app/core/<service>.js
): Singletons used to interface with external services or to provide some common utility; they are attached to the globalapp
object (for example,app.api
which handles communicating with the backend)
When writing React components, try to follow the following guidelines:
- Keep components small. If a component gets too big, it might be worth splitting it out into smaller pieces.
- Keep state to a minimum. A component without anything in
state
and onlyprops
would be best. When state is needed, make sure nothing is reduntant and can be derived from other state values. Move state upstream (to parent components) as much as it makes sense. - Use the
propTypes
attribute to document what props the component expects
See "Writing good React components".
More on state:
- The main
AppComponent
holds all of the state global to the app (like if the user is logged in or not) - Each page (
app/pages
) can hold some state specific to that page - Reusable components (
app/components
) typically hold no state (with rare exceptions, like forms)
For development, we use Connect and custom middlewares to compile and serve the app's files (see develop.js
). You can start the development server by running $ node develop
.
The web app uses Browserify to manage its code base. The main file used to create the Browserify bundle is app/app.js
.
A single "entry point" fires up the app: app.start()
. It is the only method that gets called when the code runs. This method also calls app.init(callback)
and waits for it to finish (authentication, fetching initial data, etc.) before starting the router.
This entry point is called from app/start.js
, which is not included in the Browserify app bundle.
A global window.config
object is created to hold all the config values set by the environment variables.
This is done in the app/config.js
file, which is actually a Lodash template (and is not included in the Browserify app bundle).
Third-party dependencies are managed with Bower. If a particular repository is not in the Bower registry, you can still install it by providing the URL to a tag or commit hash, for example:
bower install --save https://github.com/user/repo.git#1.1.0
Be sure to update files.js
when installing a new package. After doing so, you will also need to restart $ node develop
.
The app uses the bows library to log debugging messages to the browser's console. It is disabled by default (which makes it production-friendly). To see the messages type localStorage.debug = true
in the browser console and refresh the page. Create a logger for a particular app module by giving it a name, such as:
app.foo = {
log: bows('Foo'),
bar: function() {
this.log('Walked into bar');
}
};
Prefix all CSS classes with the component name. For example, if I'm working on the PatientList
component, I'll prefix CSS classes with patient-list-
.
Keep styles in the same folder as the component, and import them in the main app/style.less
stylesheet. If working on a "core" style, don't forget to import the files in app/core/core.less
.
In organizing the core styles in different .less
files, as well as naming core style classes, we more or less take inspiration from Twitter Bootstrap (see https://github.com/twbs/bootstrap/tree/master/less).
Some styles we'd rather not use on touch screens (for example hover effects which can be annoying while scrolling on touch screens). For that purpose, a small snippet (app/core/notouch.js
) will add the .no-touch
class to the root document element, so you can use:
.no-touch .list-item:hover {
// This will not be used on touch screens
background-color: #ccc;
}
Keep all elements and styles responsive, i.e. make sure they look good on any screen size. For media queries, we like to use the mobile-first approach, i.e. define styles for all screen sizes first, then override for bigger screen sizes. For example:
.container {
// On mobile and up, fill whole screen
width: 100%;
@media(min-width: 1024px) {
// When screen gets big enough, switch to fixed-width
width: 1024px;
margin-right: auto;
margin-left: auto;
}
}
If using class names to select elements from JavaScript (for tests, or using jQuery), prefix them with js-
. That way style changes and script changes can be done more independently.
In a separate terminal, you can watch and lint JS files with:
$ gulp jshint-watch
Images should be placed directly inside each component's directory, under an images/
subfolder. For example, the component located in the navbar/
folder, might have an image logo.png
that would be saved in navbar/images/logo.png
.
The app is then passed an IMAGES_ENDPOINT
value in the config
object, that you can use to generate the image src
attribute by just appending the component's name and the name of the image file. In our example:
var componentImageEndpoint = config.IMAGES_ENDPOINT + '/navbar';
var imageSource = componentImageEndpoint + '/logo.png';
Reusable components (app/components/
) shouldn't access the config
object directly, so you should generate the componentImageEndpoint
value above from a "page" component (app/pages
), and pass it to the reusable component through a props
value.
At build-time, images all get bundled into build/<version>/images/<component>/
directories. When adding images, don't forget to update files.js
with the correct paths.
Font files are added to the app/core/fonts
folder. The CSS rules to import the fonts are put in the Lodash template app/index.html
, because we use a configuration variable to change the URL to the font files, according to whether we are working in development or building for production.
We use an icon font for app icons (in app/core/fonts/
). To use an icon, simply add the correct class to an element (convention is to use the <i>
element), for example:
<i class="icon-logout"></i>
Take a look at the app/core/less/icons.less
file for available icons.
For local development, demoing, or testing, you can run the app in "mock" mode by setting the environment variable MOCK=true
(to turn it off use MOCK=''
). In this mode, the app will not make any calls to external services, and use dummy data contained in .json
files.
All app objects (mostly app services) that make any external call should have their methods making these external calls patched by a mock. These are located in the mock/
directory. To create one, return a patchService(service)
function (see existing mocks for examples).
Mock data is generated from .json
files, which are combined into a JavaScript object that mirrors the directory structure of the data files (for example patients/11.json
will be available at data.patients['11']
). Set the data file directory to use with the MOCK_DATA_DIR
environment variable (defaults to node_modules/blip-mock-data/default
).
You can configure the behavior of mock services using mock parameters. These are passed through the URL query string (before the hash), for example:
http://localhost:3000/?auth.skip&api.patient.getall.delay=2000#/patients
With the URL above, mock services will receive the parameters:
{
'auth.skip': true,
'api.patient.getall.delay': 2000
}
Mock parameters are very useful in development (for example, you don't necessarily want to sign in every time you refresh). They are helpful when testing (manually or automatically) different behaviors: What happens if this API call returns an empty list? What is displayed while we are waiting for data to come back from the server? Etc.
To find out which mock parameters are available, please see the corresponding service and method in the mock/
folder (look for calls to getParam()
).
The naming convention for these parameters is all lower-case, and name-spaced with periods. For example, to have the call to api.patient.getAll()
return an empty list, I would use the name api.patient.getall.empty
.
If you would like to build the app with mock parameters "baked-in", you can also use the MOCK_PARAMS
environement variable, which works like a query string (ex: $ export MOCK_PARAMS='auth.skip&api.delay=1000'
). If the same parameter is set in the URL and the environment variable, the URL's value will be used.
Fetching data from the server and rendering the UI to display that data is a classic pattern. The approach we try to follow (see The Need for Speed) is to "render as soon as possible" and "save optimistically".
In short, say a component <Items />
needs to display a data
object passed through the props by the parent, we will also give the component a fetchingData
prop, so it can render accordingly. There are 4 possible situations (the component may choose to render more than one situation in the same way):
data
is falsy andfetchingData
is truthy: first data load, or reset, we can render for example an empty "skeleton" while we wait for datadata
andfetchingData
are both falsy: data load returned an empty set, we can display a message for exampledata
is truthy andfetchingData
is falsy: display the data "normally"data
andfetchingData
are both truthy: a data refresh, either don't do anything and wait for data to come back, or display some kind of loading indicator
For forms, we try as much as possible to "save optimistically", meaning when the user "saves" the form, we immediately update the app state (and thus the UI), and then send the new data to the server to be saved. If the server returns an error, we should be able to rollback the app state and display some kind of error message.
Rules for what to cover with unit or end-to-end tests are more or less:
- Unit tests: All the small pieces, i.e. reusable UI Components and core Services
- End-to-end tests: Higher-level app behavior, which will test the main App object, the Router, and Pages
We use Mocha with Chai for the test framework, Sinon.JS and Sinon-Chai for spy, stubs, and mocks, and Testem as the test runner.
To run the tests locally, first install Testem:
$ npm install -g testem
Then run:
$ testem
This will open and run the tests in Chrome by default. You can also open other browsers and point them to the specified URL.
End-to-end (E2E) tests use Selenium for browser automation with the WebDriverJS Node.js bindings. They also use the Mocha with Chai framework.
To run E2E tests locally on Chrome, first insall the Selenium ChromeDriver:
$ make install-selenium
This will download and unzip the chromedriver
executable in the test/bin
directory.
Note: If not on Mac OSX, change the CHROMEDRIVER_ZIP
environment variable to the correct one for your OS (see the ChromeDriver downloads), and test/scripts/install_selenium.sh
).
Before running the tests, build the app (in mock mode) and start a local server in a separate terminal:
$ export MOCK=true; gulp
$ node server
(You can also run the tests in development with export MOCK=true; node develop
.)
Finally, run the tests with:
$ make test-e2e
Since E2E tests can be a little slow, you can run only a particular test by setting the E2E_TESTS
variable, for example:
$ make test-e2e E2E_TESTS=test/e2e/login_scenarios.js
We automate our builds and testing using Travis CI, and run both unit and end-to-end tests in different browsers and platforms thanks to Sauce Labs.
If you have the username and access key to our Sauce Labs account, you can also run the tests in different browsers from your local machine. Follow the instructions below, for each type of tests.
In both cases, you will need to export the Sauce Labs credentials as environment variables:
$ export SAUCE_USERNAME='...'
$ export SAUCE_ACCESS_KEY='...'
Running Sauce Labs unit tests from local machine:
-
Build the unit tests with
$ gulp before-tests
. -
(Optional) You can verify the unit tests pass in your local browser first by running
$ grunt test-server
and pointing your browser tohttp://localhost:9999/
. HitCtrl/Cmd + C
when done. -
Run the unit tests in Sauce Labs with
$ grunt test-saucelabs-local
(will spin up the test server onlocalhost:9999
and send commands to Sauce Labs).
Running Sauce Labs end-to-end tests from local machine:
-
Download Sauce Connect for your system. Unzip the archive, and copy
bin/sc
from the Sauce Connect directory to this project'stest/bin
folder. -
In a separate terminal, start Sauce Connect with
$ make sc
. -
Tell the end-to-end tests to use Sauce Labs by setting the environment variable
$ export SAUCE=true
. -
(Optional) You can specify a browser and platform to use in Sauce Labs by setting an environment variable with the pattern:
$ export BROWSER='<browserName>:<version>:<platform>'
(ex:$ export BROWSER='chrome:32:Windows 8.1
). -
Build the app and run the end-to-end tests just like you would locally (instructions above).
First load the config for the environment you wish to deploy to:
$ source config/dev.sh
Then build the static site to the dist/
directory with Gulp:
$ gulp
Note: The version
number in package.json
is used as a browser cache buster by building assets to dist/build/<version>/
.
If you want, you can test your build by running:
$ node server
After building, the dist/
directory contains files ready to be deployed to any static file server.
To deploy to Amazon S3, we recommend the Ruby gem s3_website. Install it with:
$ gem install s3_website --no-document
The tool reads configuration from environment variables through s3_website.yml
. Load the config for the environment you wish to deploy to:
$ source config/dev.sh
If the target Amazon S3 bucket is not created and configured yet, you can run:
$ s3_website cfg apply
Finally, deploy using:
$ s3_website push --site dist
Note: If asked to delete files that exist in the Amazon S3 bucket but not locally, you might want to say no. Indeed, since all app assets are self-contained in a build/<version>/
folder, only index.html
gets overwritten, and you should keep older builds around for visitors that haven't gotten the new index.html
yet.