diff --git a/.travis.yml b/.travis.yml
index 7983595abb83961fe7e6694b753357d9cd9c929c..38e14d31669cf1d2acf8f5189fd40d62728dd9c1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,7 @@
+before_script:
+  - "export DISPLAY=:99.0"
+  - "sh -e /etc/init.d/xvfb start"
+
 language: node_js
 
 before_install:
@@ -11,7 +15,9 @@ notifications:
   irc: "irc.mozilla.org#identity"
 
 env:
- - MYSQL_USER=root
+ - WHAT_TESTS=front MYSQL_USER=root
+ - WHAT_TESTS=back NODE_ENV=test_mysql MYSQL_USER=root
+ - WHAT_TESTS=back NODE_ENV=test_json
 
 mysql:
   adapter: mysql2
diff --git a/package.json b/package.json
index 8823925882dadd4dad84fb5725f3cca14fd0e249..f9483bc064ecb6d039ef69a5e793a74c9494ab19 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
     },
     "scripts": {
         "postinstall": "./scripts/generate_ephemeral_keys.sh",
-        "test": "./scripts/run_all_tests.sh",
+        "test": "./scripts/test",
         "start": "./scripts/run_locally.js"
     },
     "engines": {
diff --git a/scripts/test b/scripts/test
new file mode 100755
index 0000000000000000000000000000000000000000..cc229663e5e1a2b79682450db0aa26c8a6c75c1f
--- /dev/null
+++ b/scripts/test
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+// a script to RUN TESTS.  You can specify WHAT TESTS to run by
+// populating an environment variable 'WHAT_TESTS'.  Values include:
+//   * 'front' - frontend unit tests
+//   * 'back' - backend unit tests
+//   * 'all' - of it
+
+const
+spawn = require('child_process').spawn,
+path = require('path');
+
+// WHAT TESTS are we running?
+var whatTests = [];
+if (process.env['WHAT_TESTS']) {
+  whatTests.push(process.env['WHAT_TESTS']);
+  if (whatTests[0] == 'all') whatTests = [ 'front', 'back' ];
+} else {
+  whatTests = [ 'back' ];
+}
+
+var ec = 0;
+function run() {
+  if (!whatTests.length) process.exit(ec);
+
+  var script = {
+    front: 'test_frontend',
+    back: 'test_backend'
+  }[whatTests.shift()];
+
+  console.log(script);
+  var kid = spawn(path.join(__dirname, script));
+  kid.stdout.on('data', function(d) { process.stdout.write(d); });
+  kid.stderr.on('data', function(d) { process.stderr.write(d); });
+  kid.on('exit', function(code) {
+    if (code) process.exit(code);
+    run();
+  });
+}
+
+run();
diff --git a/scripts/run_all_tests.sh b/scripts/test_backend
similarity index 56%
rename from scripts/run_all_tests.sh
rename to scripts/test_backend
index 4e2b74d54630e78a73cea0d6ff6c7c305c509c4c..32a9dcf53be4de90aa4ab2d362461170d89ccc41 100755
--- a/scripts/run_all_tests.sh
+++ b/scripts/test_backend
@@ -17,19 +17,15 @@ fi
 # vows hates absolute paths.  sheesh.
 cd $BASEDIR
 
-for env in test_mysql test_json ; do
-  export NODE_ENV=$env
-  $SCRIPT_DIR/test_db_connectivity.js
-  if [ $? = 0 ] ; then
-      echo "Testing with NODE_ENV=$env"
-      for file in tests/*.js ; do
-          echo $file
-          vows $file
-          if [[ $? != 0 ]] ; then
-              exit 1
-          fi
-      done
-  else
-      echo "CANNOT TEST '$env' ENVIRONMENT: can't connect to the database"
-  fi
-done
+$SCRIPT_DIR/test_db_connectivity.js
+if [ $? = 0 ] ; then
+    for file in tests/*.js ; do
+        echo $file
+        vows $file
+        if [[ $? != 0 ]] ; then
+            exit 1
+        fi
+    done
+else
+    echo "CANNOT TEST '$env' ENVIRONMENT: can't connect to the database"
+fi
diff --git a/scripts/test_frontend b/scripts/test_frontend
new file mode 100755
index 0000000000000000000000000000000000000000..ac3f5465ca70b2e03c612feacbeec86dcce7f825
--- /dev/null
+++ b/scripts/test_frontend
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('../tests/lib/test_env.js');
+
+const assert =
+require('assert'),
+vows = require('vows'),
+start_stop = require('../tests/lib/start-stop.js'),
+spawn = require('child_process').spawn,
+path = require('path');
+
+var suite = vows.describe('frontend-tests');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+start_stop.addStartupBatches(suite);
+
+suite.addBatch({
+  "frontend unit tests": {
+    topic: function() {
+      // what phantom.js binary?
+      var bin = 'phantomjs';
+      try {
+        var maybe = '/usr/local/bin/phantomjs';
+        if (!fs.statSync(maybe).isFile()) throw "meh";
+        bin = maybe;
+      } catch(e) {};
+
+      var kid = spawn(
+        bin, [ path.join(__dirname, '..', 'resources', 'static', 'test', 'phantomrunner.js'),
+               'http://127.0.0.1:10002/test' ]);
+      kid.stdout.on('data', function(d) { process.stdout.write(d); });
+      kid.stderr.on('data', function(d) { process.stderr.write(d); });
+      kid.on('exit', this.callback);
+    },
+    "pass!": function(code) {
+      assert.strictEqual(code, 0);
+    }
+  }
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run({}, function(r) { process.exit(r.honored == r.total ? 0 : 1); });
+else suite.export(module);
+