diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index b3b2c815c0e86f3a64537a7a6cca6a6bfdb43456..02e790e53de50d25cb7d873843228900dce4f7de 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -249,26 +249,15 @@ div.steps {
   font-weight: normal;
 }
 
-#manage .edit {
+#manage .buttonrow {
   margin: 72px 0 14px;
 }
 
-#manageAccounts,
-#cancelManage {
-  float: right;
-  display: inline-block;
-  text-align: center;
-  line-height: 21px;
-  font-weight: bold;
+#manage .buttonrow button {
+  line-height: 20px;
+  height: 24px;
   width: 48px;
   font-size: 12px;
-  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  color: #fff;
-
-  -webkit-border-radius: 5px;
-     -moz-border-radius: 5px;
-       -o-border-radius: 5px;
-          border-radius: 5px;
 }
 
 #manageAccounts {
@@ -287,6 +276,10 @@ div.steps {
   background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #76C2FF), color-stop(100%, #37A6FF));
 }
 
+.edit #manageAccounts {
+  display: none;
+}
+
 #cancelManage {
   display: none;
   background-color: #006EC6;
@@ -304,6 +297,10 @@ div.steps {
   background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3AA7FF), color-stop(100%, #006EC6));
 }
 
+.edit #cancelManage {
+  display: inline-block;
+}
+
 #manage #emailList {
   list-style-type: none;
   margin: 0 0 72px 0;
@@ -318,7 +315,7 @@ div.steps {
 }
 
 #emailList .email,
-#emailList .activity {
+.activity {
   display: inline-block;
   float: left;
   white-space: nowrap;
@@ -328,19 +325,19 @@ div.steps {
   width: 275px;
 }
 
-#emailList .activity {
+.activity {
   width: 275px;
   color: #aaa;
   text-align: right;
 }
 
-#emailList .activity button,
-#emailList .activity abbr {
+.activity button,
+.activity abbr {
   float: right;
   display: inline-block;
 }
 
-#emailList .activity button {
+.activity button {
   width: 60px;
   margin-left: 10px;
   margin-right: -70px;
@@ -351,7 +348,7 @@ div.steps {
           transition: margin 0.4s ease;
 }
 
-#emailList.remove .activity button {
+.edit .activity button {
   margin-right: 0;
 }
 
diff --git a/resources/static/dialog/controllers/addemail.js b/resources/static/dialog/controllers/addemail.js
index e717ae7d787c0d4a2a6a5967ad7a3f28f1fb43eb..6d6ef07cbf8454f661a5a9c2f8209a8024793190 100644
--- a/resources/static/dialog/controllers/addemail.js
+++ b/resources/static/dialog/controllers/addemail.js
@@ -44,25 +44,27 @@ BrowserID.Modules.AddEmail = (function() {
       errors = bid.Errors,
       tooltip = bid.Tooltip;
 
-  function cancelEvent(event) {
-    event && event.preventDefault();
+  function cancelEvent(callback) {
+    return function(event) {
+      event && event.preventDefault();
+      callback && callback();
+    }
   }
 
-  function addEmail(event) {
+  function addEmail(callback) {
     var email = helpers.getAndValidateEmail("#newEmail"),
         self=this;
 
-    cancelEvent(event);
-
     if (email) {
-      dialogHelpers.addEmail.call(self, email);
+      dialogHelpers.addEmail.call(self, email, callback);
+    }
+    else {
+      callback && callback();
     }
   }
 
 
-  function cancelAddEmail(event) {
-    cancelEvent(event);
-
+  function cancelAddEmail() {
     this.close("cancel_state");
   }
 
@@ -72,12 +74,15 @@ BrowserID.Modules.AddEmail = (function() {
 
       self.renderDialog("addemail");
 
-      self.bind("#cancelNewEmail", "click", cancelAddEmail);
+      self.bind("#cancelNewEmail", "click", cancelEvent(cancelAddEmail));
       AddEmail.sc.start.call(self, options);
     },
-    submit: addEmail,
+    submit: addEmail
+    // START TESTING API
+    ,
     addEmail: addEmail,
     cancelAddEmail: cancelAddEmail
+    // END TESTING API
   });
 
   return AddEmail;
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index 39ae32547ede24a3cd0df70b7f8b93d1fdd08dc1..3149d5aab815d0a5ddc77a29ea7594bed203e44d 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -45,6 +45,7 @@ BrowserID.Modules.Authenticate = (function() {
       tooltip = bid.Tooltip,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
+      cancelEvent = dialogHelpers.cancelEvent,
       dom = bid.DOM,
       lastEmail = "";
 
@@ -52,12 +53,10 @@ BrowserID.Modules.Authenticate = (function() {
     return helpers.getAndValidateEmail("#email");
   }
 
-  function checkEmail(event) {
+  function checkEmail() {
     var email = getEmail(),
         self = this;
 
-    cancelEvent(event);
-
     if (!email) return;
 
     user.isEmailRegistered(email, function onComplete(registered) {
@@ -70,24 +69,22 @@ BrowserID.Modules.Authenticate = (function() {
     }, self.getErrorDialog(errors.isEmailRegistered));
   }
 
-  function createUser(event) {
+  function createUser(callback) {
     var self=this,
         email = getEmail();
 
-    cancelEvent(event);
-
     if (email) {
-      dialogHelpers.createUser.call(self, email);
+      dialogHelpers.createUser.call(self, email, callback);
+    } else {
+      callback && callback();
     }
   }
 
-  function authenticate(event) {
+  function authenticate() {
     var email = getEmail(),
         pass = helpers.getAndValidatePassword("#password"),
         self = this;
 
-    cancelEvent(event);
-
     if (email && pass) {
       dialogHelpers.authenticateUser.call(self, email, pass, function(authenticated) {
         if (authenticated) {
@@ -106,10 +103,6 @@ BrowserID.Modules.Authenticate = (function() {
     });
   }
 
-  function cancelEvent(event) {
-    if (event) event.preventDefault();
-  }
-
   function enterEmailState(el) {
     if (!el.is(":disabled")) {
       this.submit = checkEmail;
@@ -117,8 +110,7 @@ BrowserID.Modules.Authenticate = (function() {
     }
   }
 
-  function enterPasswordState(event) {
-    cancelEvent(event);
+  function enterPasswordState() {
     var self=this;
 
     self.publish("enter_password");
@@ -128,16 +120,14 @@ BrowserID.Modules.Authenticate = (function() {
     });
   }
 
-  function forgotPassword(event) {
-    cancelEvent(event);
+  function forgotPassword() {
     var email = getEmail();
     if (email) {
       this.close("forgot_password", { email: email });
     }
   }
 
-  function createUserState(event) {
-    cancelEvent(event);
+  function createUserState() {
 
     var self=this;
 
@@ -172,15 +162,18 @@ BrowserID.Modules.Authenticate = (function() {
 
 
       self.bind("#email", "keyup", emailKeyUp);
-      self.bind("#forgotPassword", "click", forgotPassword);
+      self.bind("#forgotPassword", "click", cancelEvent(forgotPassword));
 
       Authenticate.sc.start.call(self, options);
-    },
+    }
 
+    // BEGIN TESTING API
+    ,
     checkEmail: checkEmail,
     createUser: createUser,
     authenticate: authenticate,
     forgotPassword: forgotPassword
+    // END TESTING API
   });
 
   return Authenticate;
diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js
index f84368eac331f95ae9b9e88ea2650d574db6eb19..48d695f329e9534357b30cd50a7bfe6376105cdc 100644
--- a/resources/static/dialog/controllers/required_email.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -44,52 +44,52 @@ BrowserID.Modules.RequiredEmail = (function() {
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
       dom = bid.DOM,
-      assertion;
-
-  function signIn(event) {
-    event && event.preventDefault();
+      assertion,
+      cancelEvent = dialogHelpers.cancelEvent;
 
+  function signIn(callback) {
     var self = this,
         email = self.email;
 
-    // If the user is already authenticated and they own this address, sign 
+    // If the user is already authenticated and they own this address, sign
     // them right in.
     if(self.authenticated) {
-      dialogHelpers.getAssertion.call(self, email);
+      dialogHelpers.getAssertion.call(self, email, callback);
     }
     else {
-      // If the user is not already authenticated, but they potentially own 
-      // this address, try and sign them in and generate an assertion if they 
+      // If the user is not already authenticated, but they potentially own
+      // this address, try and sign them in and generate an assertion if they
       // get the password right.
       var password = helpers.getAndValidatePassword("#password");
       if (password) {
         dialogHelpers.authenticateUser.call(self, email, password, function(authenticated) {
           if (authenticated) {
-            // Now that the user has authenticated, sync their emails and get an 
+            // Now that the user has authenticated, sync their emails and get an
             // assertion for the email we care about.
             user.syncEmailKeypair(email, function() {
-              dialogHelpers.getAssertion.call(self, email);
+              dialogHelpers.getAssertion.call(self, email, callback);
             }, self.getErrorDialog(errors.syncEmailKeypair));
           }
+          else {
+            callback && callback();
+          }
         });
       }
     }
   }
 
-  function verifyAddress(event) {
-    // By being in the verifyAddress, we know that the current user  has not 
-    // been shown the password box and we have to do a verification of some 
-    // sort.  This will be either an add email to the current account or a new 
-    // registration.  
-    
-    event && event.preventDefault();
+  function verifyAddress() {
+    // By being in the verifyAddress, we know that the current user  has not
+    // been shown the password box and we have to do a verification of some
+    // sort.  This will be either an add email to the current account or a new
+    // registration.
 
     var self=this;
     if(self.authenticated) {
-      // If we are veryifying an address and the user is authenticated, it 
+      // If we are veryifying an address and the user is authenticated, it
       // means that the current user does not have control of the address.
-      // If the address is registered, it means another account has control of 
-      // the address and we are consolidating.  If the email is not registered 
+      // If the address is registered, it means another account has control of
+      // the address and we are consolidating.  If the email is not registered
       // then it means add the address to the current user's account.
       dialogHelpers.addEmail.call(self, self.email);
     }
@@ -98,17 +98,13 @@ BrowserID.Modules.RequiredEmail = (function() {
     }
   }
 
-  function forgotPassword(event) {
-    event && event.preventDefault();
-
+  function forgotPassword() {
     var self=this;
     self.close("forgot_password", { email: self.email });
   }
 
 
-  function cancel(event) {
-    event && event.preventDefault();
-
+  function cancel() {
     this.close("cancel");
   }
 
@@ -121,22 +117,28 @@ BrowserID.Modules.RequiredEmail = (function() {
       self.email = email;
       self.authenticated = authenticated;
 
-      // NOTE: When the app first starts and the user's authentication is 
-      // checked, all email addresses for authenticated users are synced.  We 
+      function ready() {
+        options.ready && options.ready();
+      }
+
+      // NOTE: When the app first starts and the user's authentication is
+      // checked, all email addresses for authenticated users are synced.  We
       // can be assured by this point that our addresses are up to date.
       if(authenticated) {
-        // if the current user owns the required email, sign in with it 
+        // if the current user owns the required email, sign in with it
         // (without a password). Otherwise, make the user verify the address
         // (which shows no password).
         var userOwnsEmail = !!user.getStoredEmailKeypair(email);
         showTemplate(userOwnsEmail, false);
+        ready();
       }
       else {
         user.isEmailRegistered(email, function(registered) {
-          // If the current email address is registered but the user is not 
-          // authenticated, make them sign in with it.  Otherwise, make them 
+          // If the current email address is registered but the user is not
+          // authenticated, make them sign in with it.  Otherwise, make them
           // verify ownership of the address.
           showTemplate(registered, registered);
+          ready();
         }, self.getErrorDialog(errors.isEmailRegistered));
       }
 
@@ -147,19 +149,22 @@ BrowserID.Modules.RequiredEmail = (function() {
           showPassword: showPassword
         });
 
-        self.bind("#sign_in", "click", signIn);
-        self.bind("#verify_address", "click", verifyAddress);
-        self.bind("#forgotPassword", "click", forgotPassword);
-        self.bind("#cancel", "click", cancel);
+        self.bind("#sign_in", "click", cancelEvent(signIn));
+        self.bind("#verify_address", "click", cancelEvent(verifyAddress));
+        self.bind("#forgotPassword", "click", cancelEvent(forgotPassword));
+        self.bind("#cancel", "click", cancelEvent(cancel));
       }
 
       RequiredEmail.sc.start.call(self, options);
-    },
+    }
 
+    // BEGIN TEST API
+    ,
     signIn: signIn,
     verifyAddress: verifyAddress,
     forgotPassword: forgotPassword,
     cancel: cancel
+    // END TEST API
   });
 
   return RequiredEmail;
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index f0cec248daf32dd9fb1e26bfd21355dc8d5a1fa0..921b24bf038803a9f11d3106ef16066458e20b87 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -130,6 +130,13 @@
     }, self.getErrorDialog(errors.addEmail));
   }
 
+  function cancelEvent(callback) {
+    return function(event) {
+      event && event.preventDefault();
+      callback.call(this);
+    };
+  }
+
   helpers.Dialog = helpers.Dialog || {};
 
   helpers.extend(helpers.Dialog, {
@@ -137,7 +144,8 @@
     authenticateUser: authenticateUser,
     createUser: createUser,
     addEmail: addEmail,
-    resetPassword: resetPassword
+    resetPassword: resetPassword,
+    cancelEvent: cancelEvent
   });
 
 }());
diff --git a/resources/static/pages/add_email_address.js b/resources/static/pages/add_email_address.js
index 010fed083044eb0f8d2d98da100924982b547e2e..d14aa7befac874cf5d338d39d38f2587bd877ab6 100644
--- a/resources/static/pages/add_email_address.js
+++ b/resources/static/pages/add_email_address.js
@@ -36,14 +36,15 @@
 
 (function() {
   "use strict";
-  
+
   var ANIMATION_TIME=250,
-      dom = BrowserID.DOM;
+      bid = BrowserID,
+      dom = bid.DOM;
 
   function emailRegistrationSuccess(info) {
 
     dom.setInner("#email", info.email);
-    
+
     if (info.origin) {
       dom.setInner(".website", info.origin);
       $(".siteinfo").show();
@@ -59,7 +60,7 @@
     $(el).fadeIn(ANIMATION_TIME);
   }
 
-  BrowserID.addEmailAddress = function(token) {
+  BrowserID.addEmailAddress = function(token, oncomplete) {
     var user = BrowserID.User;
 
     user.verifyEmail(token, function onSuccess(info) {
@@ -69,8 +70,11 @@
       else {
         showError("#cannotconfirm");
       }
+      oncomplete && oncomplete();
     }, function onFailure() {
+      // XXX This should use a real error page.
       showError("#cannotcommunicate");
+      oncomplete && oncomplete();
     });
   };
 }());
diff --git a/resources/static/pages/forgot.js b/resources/static/pages/forgot.js
index 35cae6a3e0b7117504ba22df6c2a57dad57c9e99..61d1943514405d5bad66d7bdd5d86d5ade459df7 100644
--- a/resources/static/pages/forgot.js
+++ b/resources/static/pages/forgot.js
@@ -41,12 +41,11 @@ BrowserID.forgot = (function() {
       user = bid.User,
       helpers = bid.Helpers,
       pageHelpers = bid.PageHelpers,
+      cancelEvent = pageHelpers.cancelEvent,
       dom = bid.DOM,
       tooltip = bid.Tooltip;
 
-  function submit(event) {
-    if (event) event.preventDefault();
-
+  function submit(oncomplete) {
     // GET RID OF THIS HIDE CRAP AND USE CSS!
     $(".notifications .notification").hide();
 
@@ -61,14 +60,15 @@ BrowserID.forgot = (function() {
           var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered";
           tooltip.showTooltip(tooltipEl);
         }
-      }, pageHelpers.getFailure(bid.Errors.requestPasswordReset));
+        oncomplete && oncomplete();
+      }, pageHelpers.getFailure(bid.Errors.requestPasswordReset, oncomplete));
+    } else {
+      oncomplete && oncomplete();
     }
   };
 
-  function back(event) {
-    if (event) event.preventDefault();
-
-    pageHelpers.cancelEmailSent();
+  function back(oncomplete) {
+    pageHelpers.cancelEmailSent(oncomplete);
   }
 
   function init() {
@@ -76,18 +76,20 @@ BrowserID.forgot = (function() {
 
     pageHelpers.setupEmail();
 
-    dom.bindEvent("form", "submit", submit);
-    dom.bindEvent("#back", "click", back);
+    dom.bindEvent("form", "submit", cancelEvent(submit));
+    dom.bindEvent("#back", "click", cancelEvent(back));
   }
 
+  // BEGIN TESTING API
   function reset() {
-    dom.unbindEvent("form", "submit", submit);
-    dom.unbindEvent("#back", "click", back);
+    dom.unbindEvent("form", "submit");
+    dom.unbindEvent("#back", "click");
   }
 
-  init.submit = submit; 
+  init.submit = submit;
   init.reset = reset;
   init.back = back;
+  // END TESTING API
 
   return init;
 
diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js
index 2602352af96fa2d3b8134bd88c39a4bc35f4b590..2bc2e54ff38ed7941d3d654934b7969608635ec4 100644
--- a/resources/static/pages/manage_account.js
+++ b/resources/static/pages/manage_account.js
@@ -40,7 +40,9 @@ BrowserID.manageAccount = (function() {
   var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
+      dom = bid.DOM,
       pageHelpers = bid.PageHelpers,
+      cancelEvent = pageHelpers.cancelEvent,
       confirmAction = confirm,
       doc = document;
 
@@ -126,36 +128,51 @@ BrowserID.manageAccount = (function() {
     return dObj;
   }
 
-  function syncAndDisplayEmails() {
-    var emails = {};
+  function syncAndDisplayEmails(oncomplete) {
 
     user.syncEmails(function() {
-      emails = user.getStoredEmailKeypairs();
-      if (_.isEmpty(emails)) {
-        $("#content").hide();
-      } else {
-        $("#content").show();
-        $("#vAlign").hide();
-        displayEmails(emails);
-      }
-    }, pageHelpers.getFailure(errors.syncEmails));
+      displayStoredEmails(oncomplete);
+    }, pageHelpers.getFailure(errors.syncEmails, oncomplete));
   }
 
-  function onRemoveEmail(email, event) {
-    event && event.preventDefault();
+  function displayStoredEmails(oncomplete) {
+    var emails = user.getStoredEmailKeypairs();
+    if (_.isEmpty(emails)) {
+      $("#content").hide();
+    } else {
+      $("#content").show();
+      $("#vAlign").hide();
+      displayEmails(emails);
+    }
+    oncomplete && oncomplete();
+  }
 
+  function removeEmail(email, oncomplete) {
     var emails = user.getStoredEmailKeypairs();
 
+    function complete() {
+      oncomplete && oncomplete();
+    }
+
     if (_.size(emails) > 1) {
       if (confirmAction("Remove " + email + " from your BrowserID?")) {
-        user.removeEmail(email, syncAndDisplayEmails, pageHelpers.getFailure(errors.removeEmail));
+        user.removeEmail(email, function() {
+          displayStoredEmails(oncomplete);
+        }, pageHelpers.getFailure(errors.removeEmail, oncomplete));
+      }
+      else {
+        complete();
       }
     }
     else {
-      if (confirmAction('Removing the last address will cancel your BrowserID account.\nAre you sure you want to continue?')) {
+      if (confirmAction("Removing the last address will cancel your BrowserID account.\nAre you sure you want to continue?")) {
         user.cancelUser(function() {
           doc.location="/";
-        }, pageHelpers.getFailure(errors.cancelUser));
+          complete();
+        }, pageHelpers.getFailure(errors.cancelUser, oncomplete));
+      }
+      else {
+        complete();
       }
     }
   }
@@ -163,12 +180,12 @@ BrowserID.manageAccount = (function() {
   function displayEmails(emails) {
     var list = $("#emailList").empty();
 
-    // Set up to use mustache style templating, the normal Django style blows 
+    // Set up to use mustache style templating, the normal Django style blows
     // up the node templates
     _.templateSettings = {
         interpolate : /\{\{(.+?)\}\}/g
     };
-    var template = $('#templateUser').html();
+    var template = $("#templateUser").html();
 
     _(emails).each(function(data, e) {
       var date = relativeDate(new Date(data.created));
@@ -180,59 +197,51 @@ BrowserID.manageAccount = (function() {
       });
 
       var idEl = $(identity).appendTo(list);
-      idEl.find('.delete').click(onRemoveEmail.bind(null, e));
+      idEl.find(".delete").click(cancelEvent(removeEmail.bind(null, e)));
     });
-
   }
 
-  function cancelAccount(event) {
-    event && event.preventDefault();
-
-    if (confirmAction('Are you sure you want to cancel your BrowserID account?')) {
+  function cancelAccount(oncomplete) {
+    if (confirmAction("Are you sure you want to cancel your BrowserID account?")) {
       user.cancelUser(function() {
         doc.location="/";
-      }, pageHelpers.getFailure(errors.cancelUser));
+        oncomplete && oncomplete();
+      }, pageHelpers.getFailure(errors.cancelUser, oncomplete));
     }
   }
 
-  function manageAccounts(event) {
-      event && event.preventDefault();
-
-      $('#emailList').addClass('remove');
-      $(this).hide();
-      $("#cancelManage").show();
+  function manageAccounts() {
+      $("body").addClass("edit");
   }
 
-  function cancelManage(event) {
-      event && event.preventDefault();
-
-      $('#emailList').removeClass('remove');
-      $(this).hide();
-      $("#manageAccounts").show();
+  function cancelManage() {
+      $("body").removeClass("edit");
   }
 
-  function init(options) {
+  function init(options, oncomplete) {
     options = options || {};
 
     if (options.document) doc = options.document;
     if (options.confirm) confirmAction = options.confirm;
 
-    $('#cancelAccount').click(cancelAccount);
-    $('#manageAccounts').click(manageAccounts);
-    $('#cancelManage').click(cancelManage);
+    dom.bindEvent("#cancelAccount", "click", cancelEvent(cancelAccount));
+    dom.bindEvent("#manageAccounts", "click", cancelEvent(manageAccounts));
+    dom.bindEvent("#cancelManage", "click", cancelEvent(cancelManage));
 
-    syncAndDisplayEmails();
+    syncAndDisplayEmails(oncomplete);
   }
 
+  // BEGIN TESTING API
   function reset() {
-    $('#cancelAccount').unbind("click", cancelAccount);
-    $('#manageAccounts').unbind("click", manageAccounts);
-    $('#cancelManage').unbind("click", cancelManage);
+    dom.unbindEvent("#cancelAccount", "click");
+    dom.unbindEvent("#manageAccounts", "click");
+    dom.unbindEvent("#cancelManage", "click");
   }
 
   init.reset = reset;
   init.cancelAccount = cancelAccount;
-  init.removeEmail = onRemoveEmail;
+  init.removeEmail = removeEmail;
+  // END TESTING API
 
   return init;
 
diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js
index 2f188f71788ce604a7144ceabc924c5de2ff8be4..619ef7e6f095b90561b097035f1577fdb3c8b913 100644
--- a/resources/static/pages/page_helpers.js
+++ b/resources/static/pages/page_helpers.js
@@ -87,12 +87,14 @@ BrowserID.PageHelpers = (function() {
       return decodeURIComponent(results[1].replace(/\+/g, " "));
   }
 
-  function getFailure(error) {
+  function getFailure(error, callback) {
     return function onFailure(info) {
       info = $.extend(info, { action: error, dialog: false });
       bid.Screens.error("error", info);
       $("#errorBackground").stop().fadeIn();
       $("#error").stop().fadeIn();
+
+      callback && callback();
     }
   }
 
@@ -119,10 +121,17 @@ BrowserID.PageHelpers = (function() {
     setStoredEmail(origStoredEmail);
 
     showInputs(onComplete);
-    
+
     dom.focus("input:visible:eq(0)");
   }
 
+  function cancelEvent(callback) {
+    return function(event) {
+      event && event.preventDefault();
+      callback && callback();
+    };
+  }
+
   return {
     setupEmail: prefillEmail,
     setStoredEmail: setStoredEmail,
@@ -133,6 +142,7 @@ BrowserID.PageHelpers = (function() {
     replaceInputsWithNotice: replaceInputsWithNotice,
     showInputs: showInputs,
     showEmailSent: showEmailSent,
-    cancelEmailSent: cancelEmailSent
+    cancelEmailSent: cancelEmailSent,
+    cancelEvent: cancelEvent
   };
 }());
diff --git a/resources/static/pages/signin.js b/resources/static/pages/signin.js
index ace9cc369fb81bc3a123e55cec75bdc7b592663d..94308efc780ecdd9d019a7f9ecc2a2a22fc44ccf 100644
--- a/resources/static/pages/signin.js
+++ b/resources/static/pages/signin.js
@@ -42,11 +42,10 @@ BrowserID.signIn = (function() {
       helpers = bid.Helpers,
       dom = bid.DOM,
       pageHelpers = bid.PageHelpers,
+      cancelEvent = pageHelpers.cancelEvent,
       doc = document;
 
-  function submit(event) {
-    if (event) event.preventDefault();
-
+  function submit(oncomplete) {
     var email = helpers.getAndValidateEmail("#email"),
         password = helpers.getAndValidatePassword("#password");
 
@@ -60,7 +59,11 @@ BrowserID.signIn = (function() {
           // bad authentication
           $(".notifications .notification.badlogin").fadeIn();
         }
-      }, pageHelpers.getFailure(bid.Errors.authenticate));
+        oncomplete && oncomplete();
+      }, pageHelpers.getFailure(bid.Errors.authenticate, oncomplete));
+    }
+    else {
+      oncomplete && oncomplete();
     }
   }
 
@@ -71,15 +74,17 @@ BrowserID.signIn = (function() {
 
     pageHelpers.setupEmail();
 
-    dom.bindEvent("form", "submit", submit);
+    dom.bindEvent("form", "submit", cancelEvent(submit));
   }
 
+  // BEGIN TESTING API
   function reset() {
-    dom.unbindEvent("form", "submit", submit);
+    dom.unbindEvent("form", "submit");
   }
 
   init.submit = submit;
   init.reset = reset;
+  // END TESTING API
 
   return init;
 
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index f6ab274298f41a558fe1d7c7b86498f403a7cfaf..3f9f7876f997b43b3367a1de14011b3d786df1b2 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -42,6 +42,7 @@ BrowserID.signUp = (function() {
       dom = bid.DOM,
       helpers = bid.Helpers,
       pageHelpers = bid.PageHelpers,
+      cancelEvent = pageHelpers.cancelEvent,
       errors = bid.Errors,
       tooltip = BrowserID.Tooltip,
       ANIMATION_SPEED = 250,
@@ -51,34 +52,40 @@ BrowserID.signUp = (function() {
       $(selector).fadeIn(ANIMATION_SPEED);
     }
 
-    function submit(event) { 
-      if (event) event.preventDefault();
-
+    function submit(oncomplete) {
       var email = helpers.getAndValidateEmail("#email");
+
+      function complete() {
+        oncomplete && oncomplete();
+      }
+
       if (email) {
         user.isEmailRegistered(email, function(registered) {
           if (!registered) {
             user.createUser(email, function onSuccess(success) {
               if(success) {
-                pageHelpers.showEmailSent();
+                pageHelpers.showEmailSent(oncomplete);
               }
               else {
                 tooltip.showTooltip("#could_not_add");
+                complete();
               }
-            }, pageHelpers.getFailure(errors.createUser));
+            }, pageHelpers.getFailure(errors.createUser, oncomplete));
           }
           else {
             $('#registeredEmail').html(email);
             showNotice(".alreadyRegistered");
+            complete();
           }
-        }, pageHelpers.getFailure(errors.isEmailRegistered));
+        }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete));
+      }
+      else {
+        complete();
       }
     }
 
-    function back(event) {
-      if (event) event.preventDefault();
-
-      pageHelpers.cancelEmailSent();
+    function back(oncomplete) {
+      pageHelpers.cancelEmailSent(oncomplete);
     }
 
     function onEmailKeyUp(event) {
@@ -91,19 +98,21 @@ BrowserID.signUp = (function() {
       pageHelpers.setupEmail();
 
       dom.bindEvent("#email", "keyup", onEmailKeyUp);
-      dom.bindEvent("form", "submit", submit);
-      dom.bindEvent("#back", "click", back);
+      dom.bindEvent("form", "submit", cancelEvent(submit));
+      dom.bindEvent("#back", "click", cancelEvent(back));
     }
 
+    // BEGIN TESTING API
     function reset() {
-      dom.unbindEvent("#email", "keyup", onEmailKeyUp);
-      dom.unbindEvent("form", "submit", submit);
-      dom.unbindEvent("#back", "click", back);
+      dom.unbindEvent("#email", "keyup");
+      dom.unbindEvent("form", "submit");
+      dom.unbindEvent("#back", "click");
     }
 
     init.submit = submit;
     init.reset = reset;
     init.back = back;
+    // END TESTING API
 
     return init;
 }());
diff --git a/resources/static/pages/verify_email_address.js b/resources/static/pages/verify_email_address.js
index 6083e06c271dd4b8d284d1639e910fddefbdfcb4..9085eaf001f7868e60490d5857b8bddd2478c19e 100644
--- a/resources/static/pages/verify_email_address.js
+++ b/resources/static/pages/verify_email_address.js
@@ -47,9 +47,7 @@
     $("#signUpForm").remove();
   }
 
-  function submit(event) {
-    if (event) event.preventDefault();
-
+  function submit(oncomplete) {
     var pass = $("#password").val(),
         vpass = $("#vpassword").val();
 
@@ -64,12 +62,16 @@
         else {
           showError("#cannotcomplete");
         }
-      }, pageHelpers.getFailure(errors.completeUserRegistration));
+        oncomplete && oncomplete();
+      }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
+    }
+    else {
+      oncomplete && oncomplete();
     }
   }
 
-  function init(tok) {
-    $("#signUpForm").bind("submit", submit);
+  function init(tok, oncomplete) {
+    $("#signUpForm").bind("submit", pageHelpers.cancelEvent(submit));
     $(".siteinfo").hide();
     $("#congrats").hide();
     token = tok;
@@ -88,11 +90,12 @@
       else {
         showError("#cannotconfirm");
       }
-    }, pageHelpers.getFailure(errors.completeUserRegistration));
+      oncomplete && oncomplete();
+    }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
   }
 
   function reset() {
-    $("#signUpForm").unbind("submit", submit);
+    $("#signUpForm").unbind("submit");
   }
 
   init.submit = submit;
diff --git a/resources/static/shared/mediator.js b/resources/static/shared/mediator.js
index ab026a1046d8131ccf5db934a4c95c4069c6f3b5..bfe6f15e0491c3f3c09c6648727f600869028a61 100644
--- a/resources/static/shared/mediator.js
+++ b/resources/static/shared/mediator.js
@@ -40,7 +40,8 @@ BrowserID.Mediator = (function() {
   return {
     subscribe: hub.on.bind(hub),
     unsubscribe: hub.off.bind(hub),
-    publish: hub.fire.bind(hub)
+    publish: hub.fire.bind(hub),
+    reset: hub.reset.bind(hub)
   };
 }());
 
diff --git a/resources/static/test/qunit.html b/resources/static/test/qunit.html
index b05ae11393b474a044a436b03e3c83b798513a02..eec948c72d5abb059e37664963586f63b2791890 100644
--- a/resources/static/test/qunit.html
+++ b/resources/static/test/qunit.html
@@ -125,6 +125,8 @@
 
     <script type="text/javascript" src="/relay/relay.js"></script>
 
+    <script type="text/javascript" src="qunit/testHelpers/helpers.js"></script>
+
     <script type="text/javascript" src="qunit/include_unit_test.js"></script>
     <script type="text/javascript" src="qunit/relay/relay_unit_test.js"></script>
 
diff --git a/resources/static/test/qunit/controllers/addemail_unit_test.js b/resources/static/test/qunit/controllers/addemail_unit_test.js
index 75090e022f8b606f5704832fdf18bbc484a20842..c5e238ac222222ee6ce26c69d9723dc0966c8ffa 100644
--- a/resources/static/test/qunit/controllers/addemail_unit_test.js
+++ b/resources/static/test/qunit/controllers/addemail_unit_test.js
@@ -37,35 +37,22 @@
 (function() {
   "use strict";
 
-  var controller, 
+  var controller,
       el = $("body"),
       bid = BrowserID,
       storage = bid.Storage,
       user = bid.User,
       network = bid.Network,
-      xhr = bid.Mocks.xhr,
-      mediator = bid.Mediator, 
       modules = bid.Modules,
       testOrigin = "http://browserid.org",
-      registrations = [];
+      register = bid.TestHelpers.register;
 
-  function register(message, cb) {
-    registrations.push(mediator.subscribe(message, cb));
-  }
-
-  function unregisterAll() {
-    var registration;
-    while(registration = registrations.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
 
-  module("controllers/addemail_controller", {
+  module("controllers/addemail", {
     setup: function() {
-      network.setXHR(xhr);
-      xhr.useResult("valid");
-      storage.clear();
+      $("#newEmail").val("");
       user.setOrigin(testOrigin);
+      bid.TestHelpers.setup();
     },
 
     teardown: function() {
@@ -76,10 +63,8 @@
         } catch(e) {
           // could already be destroyed from the close
         }
-      }    
-      network.setXHR($);
-      storage.clear();
-      unregisterAll();
+      }
+      bid.TestHelpers.teardown();
     }
   });
 
@@ -94,12 +79,13 @@
     equal($("#addEmail").length, 1, "control rendered correctly");
 
     $("#newEmail").val("unregistered@testuser.com");
+
     register("email_staged", function(msg, info) {
       equal(info.email, "unregistered@testuser.com", "email_staged called with correct email");
       start();
     });
+
     controller.addEmail();
-    
   });
 
   asyncTest("addEmail with valid email with leading/trailing whitespace", function() {
@@ -111,7 +97,6 @@
       start();
     });
     controller.addEmail();
-    
   });
 
   asyncTest("addEmail with invalid email", function() {
@@ -122,14 +107,11 @@
     register("email_staged", function(msg, info) {
       handlerCalled = true;
       ok(false, "email_staged should not be called on invalid email");
-      start();
     });
-    controller.addEmail();
-    setTimeout(function() {
+    controller.addEmail(function() {
       equal(handlerCalled, false, "the email_staged handler should have never been called");
       start();
-    }, 100);
-    
+    });
   });
 
   asyncTest("addEmail with previously registered email - allows for account consolidation", function() {
@@ -141,7 +123,7 @@
       start();
     });
     controller.addEmail();
-    
+
   });
 
   asyncTest("cancelAddEmail", function() {
@@ -152,7 +134,7 @@
       start();
     });
     controller.cancelAddEmail();
-    
+
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
index cd172990cc4b156529fe77acc52c945b74e3ae2d..497145e20ccbb61953810869cc0b14e48483da2e 100644
--- a/resources/static/test/qunit/controllers/authenticate_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -46,25 +46,10 @@
       emailRegistered = false,
       userCreated = true,
       mediator = bid.Mediator,
-      registrations = [];
-
-  function register(message, cb) {
-    registrations.push(mediator.subscribe(message, cb));
-  }
-
-  function unregisterAll() {
-    var registration;
-    while(registration = registrations.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
+      registrations = [],
+      register = bid.TestHelpers.register;
 
   function reset() {
-    el = $("#controller_head");
-    el.find("#formWrap .contents").html("");
-    el.find("#wait .contents").html("");
-    el.find("#error .contents").html("");
-
     emailRegistered = false;
     userCreated = true;
   }
@@ -75,12 +60,10 @@
     controller.start(options);
   }
 
-  module("controllers/authenticate_controller", {
+  module("controllers/authenticate", {
     setup: function() {
       reset();
-      storage.clear();
-      network.setXHR(xhr);
-      xhr.useResult("valid");
+      bid.TestHelpers.setup();
       createController();
     },
 
@@ -93,9 +76,7 @@
         }
       }
       reset();
-      storage.clear();
-      network.setXHR($);
-      unregisterAll();
+      bid.TestHelpers.teardown();
     }
   });
 
@@ -164,7 +145,7 @@
 
     register("forgot_password", function(msg, info) {
       equal(info.email, "registered@testuser.com", "forgot_password with correct email triggered");
-      start();  
+      start();
     });
 
     controller.forgotPassword();
@@ -174,11 +155,10 @@
     $("#email").val("unregistered@testuser.com");
     register("user_staged", function(msg, info) {
       equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered");
-      start();  
+      start();
     });
 
     controller.createUser();
-    
   });
 
   asyncTest("createUser with invalid email", function() {
@@ -189,13 +169,10 @@
       handlerCalled = true;
     });
 
-    controller.createUser();
-    setTimeout(function() {
+    controller.createUser(function() {
       equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email");
-      start();  
-    }, 100);
-
-    
+      start();
+    });
   });
 
   asyncTest("createUser with valid email but throttling", function() {
@@ -207,14 +184,13 @@
     });
 
     xhr.useResult("throttle");
-    controller.createUser();
-    setTimeout(function() {
+    controller.createUser(function() {
       equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling");
       equal(bid.Tooltip.shown, true, "tooltip is shown");
-      start();  
-    }, 100);
+      start();
+    });
+
 
-    
   });
 
   asyncTest("createUser with valid email, XHR error", function() {
@@ -226,13 +202,12 @@
     });
 
     xhr.useResult("ajaxError");
-    controller.createUser();
+    controller.createUser()
+
     setTimeout(function() {
       equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error");
-      start();  
-    }, 100);
-
-    
+      start();
+    }, 50);
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/checkregistration_unit_test.js b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
index 1074f7c15fab4685e76ae2a82bb0cb36c09f4978..6fa6920cfa331ea4f8961b8c421de016a9137116 100644
--- a/resources/static/test/qunit/controllers/checkregistration_unit_test.js
+++ b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
@@ -41,19 +41,7 @@
       bid = BrowserID,
       xhr = bid.Mocks.xhr,
       network = bid.Network,
-      mediator = bid.Mediator,
-      listeners = [];
-
-  function subscribe(message, cb) {
-    listeners.push(mediator.subscribe(message, cb));
-  }
-
-  function unsubscribeAll() {
-    var registration;
-    while(registration = listeners.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
+      register = bid.TestHelpers.register;
 
   function createController(verifier, message) {
     controller = bid.Modules.CheckRegistration.create();
@@ -66,27 +54,23 @@
 
   module("controllers/checkregistration_controller", {
     setup: function() {
-      xhr.useResult("valid");
-      network.setXHR(xhr);
-      $("#error").hide();
+      bid.TestHelpers.setup();
     },
 
     teardown: function() {
-      network.setXHR($);
+      bid.TestHelpers.teardown();
       if (controller) {
         try {
           // Controller may have already destroyed itself.
           controller.destroy();
         } catch(e) {}
       }
-      unsubscribeAll();
-      $("#error").hide();
-    } 
+    }
   });
 
   function testVerifiedUserEvent(event_name, message) {
     createController("waitForUserValidation", event_name);
-    subscribe(event_name, function() {
+    register(event_name, function() {
       ok(true, message);
       start();
     });
@@ -112,29 +96,27 @@
     xhr.useResult("ajaxError");
 
     createController("waitForUserValidation", "user_verified");
-    subscribe("user_verified", function() {
+    register("user_verified", function() {
       ok(false, "on XHR error, should not complete");
     });
     controller.startCheck();
-    
+
     setTimeout(function() {
       ok($("#error").is(":visible"), "Error message is visible");
       start();
-    }, 1000);
-
-    
+    }, 500);
   });
 
   asyncTest("cancel raises cancel_state", function() {
     createController("waitForUserValidation", "user_verified");
-    subscribe("cancel_state", function() {
+    register("cancel_state", function() {
       ok(true, "on cancel, cancel_state is triggered");
       start();
     });
     controller.startCheck();
     controller.cancel();
 
-    
+
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/dialog_unit_test.js b/resources/static/test/qunit/controllers/dialog_unit_test.js
index 33ed6facba43c807fc5edc3390bda9f8d52a6428..23941014f4bc74e8533f55d88914cff4aef97623 100644
--- a/resources/static/test/qunit/controllers/dialog_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_unit_test.js
@@ -38,7 +38,6 @@
   "use strict";
 
   var bid = BrowserID,
-      mediator = bid.Mediator,
       channel = bid.Channel,
       controller,
       el,
@@ -47,16 +46,11 @@
       navMock;
 
   function reset() {
-    el = $("#controller_head");
-    el.find("#formWrap .contents").html("");
-    el.find("#wait .contents").html("");
-    el.find("#error .contents").html("");
-
     channelError = false;
   }
 
   function WinMock() {
-    this.location.hash = "#1234";  
+    this.location.hash = "#1234";
   }
 
   WinMock.prototype = {
@@ -103,6 +97,7 @@
         navigator: navMock
       });
       reset();
+      bid.TestHelpers.setup();
     },
 
     teardown: function() {
@@ -112,6 +107,7 @@
         window: window,
         navigator: navigator
       });
+      bid.TestHelpers.teardown();
     }
   });
 
@@ -163,7 +159,7 @@
   /*
   test("doCheckAuth with registered requiredEmail, authenticated", function() {
     createController({
-      requiredEmail: "registered@testuser.com" 
+      requiredEmail: "registered@testuser.com"
     });
 
     controller.doCheckAuth();
@@ -171,7 +167,7 @@
 
   test("doCheckAuth with registered requiredEmail, not authenticated", function() {
     createController({
-      requiredEmail: "registered@testuser.com" 
+      requiredEmail: "registered@testuser.com"
     });
 
     controller.doCheckAuth();
@@ -179,7 +175,7 @@
 
   test("doCheckAuth with unregistered requiredEmail, not authenticated", function() {
     createController({
-      requiredEmail: "unregistered@testuser.com" 
+      requiredEmail: "unregistered@testuser.com"
     });
 
     controller.doCheckAuth();
@@ -187,7 +183,7 @@
 
   test("doCheckAuth with unregistered requiredEmail, authenticated as other user", function() {
     createController({
-      requiredEmail: "unregistered@testuser.com" 
+      requiredEmail: "unregistered@testuser.com"
     });
 
     controller.doCheckAuth();
diff --git a/resources/static/test/qunit/controllers/forgotpassword_unit_test.js b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
index c2ee8b30918f9c34f71c82b6db3c135c5364c619..e2e82d71b920b942ca7852384f0ca75ef05a7b4b 100644
--- a/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
+++ b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
@@ -40,29 +40,7 @@
   var controller,
       el = $("body"),
       bid = BrowserID,
-      storage = bid.Storage,
-      network = bid.Network,
-      xhr = bid.Mocks.xhr,
-      mediator = bid.Mediator,
-      registrations = [];
-
-  function register(message, cb) {
-    registrations.push(mediator.subscribe(message, cb));
-  }
-
-  function unregisterAll() {
-    var registration;
-    while(registration = registrations.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
-
-  function reset() {
-    el = $("#controller_head");
-    el.find("#formWrap .contents").html("");
-    el.find("#wait .contents").html("");
-    el.find("#error .contents").html("");
-  }
+      register = bid.TestHelpers.register;
 
   function createController(options) {
     controller = bid.Modules.ForgotPassword.create();
@@ -72,10 +50,7 @@
   module("controllers/forgotpassword_controller", {
     setup: function() {
       $("#email").val("");
-      reset();
-      storage.clear();
-      network.setXHR(xhr);
-      xhr.useResult("valid");
+      bid.TestHelpers.setup();
       createController({ email: "registered@testuser.com" });
     },
 
@@ -88,10 +63,7 @@
           // may already be destroyed from close inside of the controller.
         }
       }
-      reset();
-      storage.clear();
-      network.setXHR($);
-      unregisterAll();
+      bid.TestHelpers.setup();
     }
   });
 
@@ -106,7 +78,7 @@
     });
 
     controller.resetPassword();
-    
+
   });
 
   asyncTest("cancelResetPassword raises 'cancel_forgot_password'", function() {
@@ -116,7 +88,7 @@
     });
 
     controller.cancelResetPassword();
-    
+
   });
 }());
 
diff --git a/resources/static/test/qunit/controllers/page_unit_test.js b/resources/static/test/qunit/controllers/page_unit_test.js
index 826adf66559e36de5f26468bec4ca10b662dc470..ea295702e90757d4e0640f899a39e3e3876d0fc4 100644
--- a/resources/static/test/qunit/controllers/page_unit_test.js
+++ b/resources/static/test/qunit/controllers/page_unit_test.js
@@ -41,16 +41,7 @@
       bodyTemplate = "testBodyTemplate",
       waitTemplate = "wait",
       bid = BrowserID,
-      mediator = bid.Mediator,
-      modules = bid.Modules;
-
-  function reset() {
-    el = $("#controller_head");
-    el.find("#formWrap .contents").html("");
-    el.find("#wait .contents").html("");
-    $("#error").html("<div class='contents'></div>");
-    el.find("#error .contents").html("");
-  }
+      mediator = bid.Mediator;
 
   function createController(options) {
     controller = bid.Modules.PageModule.create(options);
@@ -59,12 +50,13 @@
 
   module("controllers/page_controller", {
     setup: function() {
-      reset();
+      el = $("#controller_head");
+      bid.TestHelpers.setup();
     },
 
     teardown: function() {
       controller.destroy();
-      reset();
+      bid.TestHelpers.teardown();
     }
   });
 
@@ -231,7 +223,7 @@
     setTimeout(function() {
       equal(listenerCalled, false, "all events are unbound, listener should not be called");
       start();
-    }, 100);
+    }, 1);
   });
 
   asyncTest("publish", function() {
diff --git a/resources/static/test/qunit/controllers/pickemail_unit_test.js b/resources/static/test/qunit/controllers/pickemail_unit_test.js
index 73c97c7a54ce8a4956cb96facad3ee1296f6379c..d7c4886a24caec23352d723dbf0bc2051de4c6a9 100644
--- a/resources/static/test/qunit/controllers/pickemail_unit_test.js
+++ b/resources/static/test/qunit/controllers/pickemail_unit_test.js
@@ -37,41 +37,18 @@
 (function() {
   "use strict";
 
-  var controller, 
+  var controller,
       el = $("body"),
       bid = BrowserID,
       storage = bid.Storage,
       user = bid.User,
-      network = bid.Network,
-      xhr = bid.Mocks.xhr,
-      mediator = bid.Mediator,
       testOrigin = "http://browserid.org",
-      registrations = [];
-
-  function register(message, cb) {
-    registrations.push(mediator.subscribe(message, cb));
-  }
-
-  function unregisterAll() {
-    var registration;
-    while(registration = registrations.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
-
-  function reset() {
-    el = $("#controller_head");
-    el.find("#formWrap .contents").html("");
-    el.find("#wait .contents").html("");
-    el.find("#error .contents").html("");
-  }
+      register = bid.TestHelpers.register;
 
   module("controllers/pickemail_controller", {
     setup: function() {
-      reset();
-      network.setXHR(xhr);
-      xhr.useResult("valid");
-      storage.clear();
+      el = $("#controller_head");
+      bid.TestHelpers.setup();
       user.setOrigin(testOrigin);
     },
 
@@ -83,12 +60,9 @@
         } catch(e) {
           // could already be destroyed from the close
         }
-      }    
-      network.setXHR($);
-      reset();
-      storage.clear();
-      unregisterAll();
-    } 
+      }
+      bid.TestHelpers.setup();
+    }
   });
 
 
diff --git a/resources/static/test/qunit/controllers/required_email_unit_test.js b/resources/static/test/qunit/controllers/required_email_unit_test.js
index 8b1e40862d4cf1f7725d37f84d1212c8eaf99fa5..8ffc24adda46615584cf4e1a9fde664dc12a7b62 100644
--- a/resources/static/test/qunit/controllers/required_email_unit_test.js
+++ b/resources/static/test/qunit/controllers/required_email_unit_test.js
@@ -42,30 +42,12 @@
       bid = BrowserID,
       xhr = bid.Mocks.xhr,
       user = bid.User,
-      network = bid.Network,
-      storage = bid.Storage,
-      mediator = bid.Mediator,
-      listeners = [];
-
-  // XXX TODO Share this code with the other tests.
-  function subscribe(message, cb) {
-    listeners.push(mediator.subscribe(message, cb));
-  }
-
-  function unsubscribeAll() {
-    var registration;
-    while(registration = listeners.pop()) {
-      mediator.unsubscribe(registration);
-    }
-  }
+      register = bid.TestHelpers.register;
 
   module("controllers/required_email", {
     setup: function() {
       el = $("body");
-      $("#error").hide();
-      network.setXHR(xhr);
-      storage.clear();
-      xhr.useResult("valid");
+      bid.TestHelpers.setup();
       xhr.setContextInfo({
         authenticated: false
       });
@@ -81,8 +63,7 @@
         }
         controller = null;
       }
-      network.setXHR($);
-      unsubscribeAll();
+      bid.TestHelpers.setup();
     }
   });
 
@@ -92,26 +73,22 @@
   }
 
   function testSignIn(email, cb) {
-    setTimeout(function() {
-      var el = $("#required_email");
-      equal(el.val() || el.text(), email, "email set correctly");
-      equal($("#sign_in").length, 1, "sign in button shown");
-      equal($("#verify_address").length, 0, "verify address not shows");
-      cb && cb();
-      start();
-    }, 500);
+    var el = $("#required_email");
+    equal(el.val() || el.text(), email, "email set correctly");
+    equal($("#sign_in").length, 1, "sign in button shown");
+    equal($("#verify_address").length, 0, "verify address not shows");
+    cb && cb();
+    start();
   }
 
   function testVerify(email, cb) {
-    setTimeout(function() {
-      var el = $("#required_email");
-      equal(el.val() || el.text(), email, "email set correctly");
-      equal($("#sign_in").length, 0, "sign in button not shown");
-      equal($("#verify_address").length, 1, "verify address shows");
-      testNoPasswordSection();
-      cb && cb();
-      start();
-    }, 500);
+    var el = $("#required_email");
+    equal(el.val() || el.text(), email, "email set correctly");
+    equal($("#sign_in").length, 0, "sign in button not shown");
+    equal($("#verify_address").length, 1, "verify address shows");
+    testNoPasswordSection();
+    cb && cb();
+    start();
   }
 
   function testPasswordSection() {
@@ -126,20 +103,24 @@
     var email = "registered@testuser.com";
     createController({
       email: email,
-      authenticated: false
+      authenticated: false,
+      ready: function() {
+        testSignIn(email, testPasswordSection);
+      }
     });
 
-    testSignIn(email, testPasswordSection);
   });
 
   asyncTest("user who is not authenticated, email not registered", function() {
     var email = "unregistered@testuser.com";
     createController({
       email: email,
-      authenticated: false
+      authenticated: false,
+      ready: function() {
+        testVerify(email);
+      }
     });
 
-    testVerify(email);
   });
 
   asyncTest("user who is not authenticated, XHR error", function() {
@@ -147,7 +128,9 @@
     var email = "registered@testuser.com";
     createController({
       email: email,
-      authenticated: false
+      authenticated: false,
+      ready: function() {
+      }
     });
 
     setTimeout(function() {
@@ -165,11 +148,13 @@
     user.syncEmailKeypair(email, function() {
       createController({
         email: email,
-        authenticated: true
+        authenticated: true,
+        ready: function() {
+          testSignIn(email, testNoPasswordSection);
+        }
       });
     });
 
-    testSignIn(email, testNoPasswordSection);
   });
 
   asyncTest("user who is authenticated, email belongs to another user", function() {
@@ -180,12 +165,13 @@
     var email = "registered@testuser.com";
     createController({
       email: email,
-      authenticated: true
+      authenticated: true,
+      ready: function() {
+        // This means the current user is going to take the address from the other
+        // account.
+        testVerify(email);
+      }
     });
-
-    // This means the current user is going to take the address from the other
-    // account.
-    testVerify(email);
   });
 
   asyncTest("user who is authenticated, but email unknown", function() {
@@ -196,10 +182,11 @@
     var email = "unregistered@testuser.com";
     createController({
       email: email,
-      authenticated: true
+      authenticated: true,
+      ready: function() {
+        testVerify(email);
+      }
     });
-
-    testVerify(email);
   });
 
 
@@ -215,7 +202,7 @@
         authenticated: true
       });
 
-      subscribe("assertion_generated", function(item, info) {
+      register("assertion_generated", function(item, info) {
         ok(info.assertion, "we have an assertion");
         start();
       });
@@ -224,7 +211,7 @@
     });
   });
 
-  asyncTest("signIn of an non-authenticated user with a good password generates an assertion", function() {
+  asyncTest("signIn of a non-authenticated user with a good password generates an assertion", function() {
     xhr.setContextInfo({
       authenticated: false
     });
@@ -232,20 +219,22 @@
     var email = "registered@testuser.com";
     createController({
       email: email,
-      authenticated: false
-    });
-
-    subscribe("assertion_generated", function(item, info) {
-      ok(info.assertion, "we have an assertion");
-      start();
+      authenticated: false,
+      ready: function() {
+        register("assertion_generated", function(item, info) {
+          ok(info.assertion, "we have an assertion");
+          start();
+        });
+
+        $("#password").val("password");
+        controller.signIn();
+      }
     });
 
-    $("#password").val("password");
-    controller.signIn();
   });
 
 
-  asyncTest("signIn of an non-authenticated user with a bad password does not generate an assertion", function() {
+  asyncTest("signIn of a non-authenticated user with a bad password does not generate an assertion", function() {
     xhr.setContextInfo({
       authenticated: false
     });
@@ -253,27 +242,26 @@
     var email = "registered@testuser.com";
     createController({
       email: email,
-      authenticated: false
-    });
-
-    var assertion;
-
-    subscribe("assertion_generated", function(item, info) {
-      ok(false, "this should not have been called");
-      assertion = info.assertion;
+      authenticated: false,
+      ready: function() {
+        var assertion;
+
+        register("assertion_generated", function(item, info) {
+          ok(false, "this should not have been called");
+          assertion = info.assertion;
+        });
+
+        xhr.useResult("invalid");
+        $("#password").val("badpassword");
+        controller.signIn(function() {
+          // Since we are using the mock, we know the XHR result is going to be
+          // back in less than 1000ms.  All we have to do is check whether an
+          // assertion was generated, if so, bad jiji.
+          equal(typeof assertion, "undefined", "assertion was never generated");
+          start();
+        });
+      }
     });
-
-    xhr.useResult("invalid");
-    $("#password").val("badpassword");
-    controller.signIn();
-
-    setTimeout(function() {
-      // Since we are using the mock, we know the XHR result is going to be
-      // back in less than 1000ms.  All we have to do is check whether an
-      // assertion was generated, if so, bad jiji.
-      equal(typeof assertion, "undefined", "assertion was never generated");
-      start();
-    }, 1000);
   });
 
   function testMessageReceived(email, message) {
@@ -285,16 +273,16 @@
 
     createController({
       email: email,
-      authenticated: authenticated
-    });
-
-
-    subscribe(message, function(item, info) {
-      equal(info.email, email, message + " received with correct email");
-      start();
+      authenticated: authenticated,
+      ready: function() {
+        register(message, function(item, info) {
+          equal(info.email, email, message + " received with correct email");
+          start();
+        });
+
+        controller.verifyAddress();
+      }
     });
-
-    controller.verifyAddress();
   }
 
   asyncTest("verifyAddress of authenticated user, address belongs to another user", function() {
@@ -320,16 +308,16 @@
 
     createController({
       email: email,
-      authenticated: authenticated
-    });
-
-
-    subscribe(message, function(item, info) {
-      equal(info.email, email, message + " received with correct email");
-      start();
+      authenticated: authenticated,
+      ready: function() {
+        register(message, function(item, info) {
+          equal(info.email, email, message + " received with correct email");
+          start();
+        });
+
+        controller.forgotPassword();
+      }
     });
-
-    controller.forgotPassword();
   });
 
   asyncTest("cancel raises the cancel message", function() {
@@ -343,16 +331,16 @@
 
     createController({
       email: email,
-      authenticated: authenticated
-    });
-
-
-    subscribe(message, function(item, info) {
-      ok(true, message + " received");
-      start();
+      authenticated: authenticated,
+      ready: function() {
+        register(message, function(item, info) {
+          ok(true, message + " received");
+          start();
+        });
+
+        controller.cancel();
+      }
     });
-
-    controller.cancel();
   });
 
 }());
diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js
index 6b803c57f7fd0ad37b2857ca54f7101d07f805ff..7a5a5d865a3ff54b4a1bc23cddac665edf5a123a 100644
--- a/resources/static/test/qunit/mocks/xhr.js
+++ b/resources/static/test/qunit/mocks/xhr.js
@@ -47,25 +47,25 @@ BrowserID.Mocks.xhr = (function() {
   var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
 
   /**
-   * This is the results table, the keys are the request type, url, and 
-   * a "selector" for testing.  The right is the expected return value, already 
-   * decoded.  If a result is "undefined", the request's error handler will be 
+   * This is the results table, the keys are the request type, url, and
+   * a "selector" for testing.  The right is the expected return value, already
+   * decoded.  If a result is "undefined", the request's error handler will be
    * called.
    */
   var xhr = {
     results: {
       "get /wsapi/session_context valid": contextInfo,
       "get /wsapi/session_context invalid": contextInfo,
-      // We are going to test for XHR failures for session_context using 
+      // We are going to test for XHR failures for session_context using
       // call to serverTime.  We are going to use the flag contextAjaxError
-      "get /wsapi/session_context ajaxError": contextInfo, 
-      "get /wsapi/session_context complete": contextInfo, 
-      "get /wsapi/session_context throttle": contextInfo, 
-      "get /wsapi/session_context multiple": contextInfo, 
-      "get /wsapi/session_context no_identities": contextInfo, 
-      "get /wsapi/session_context contextAjaxError": undefined,  
-      "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },  
-      "get /wsapi/email_for_token?token=token invalid": { success: false },  
+      "get /wsapi/session_context ajaxError": contextInfo,
+      "get /wsapi/session_context complete": contextInfo,
+      "get /wsapi/session_context throttle": contextInfo,
+      "get /wsapi/session_context multiple": contextInfo,
+      "get /wsapi/session_context no_identities": contextInfo,
+      "get /wsapi/session_context contextAjaxError": undefined,
+      "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },
+      "get /wsapi/email_for_token?token=token invalid": { success: false },
       "post /wsapi/authenticate_user valid": { success: true },
       "post /wsapi/authenticate_user invalid": { success: false },
       "post /wsapi/authenticate_user ajaxError": undefined,
@@ -154,7 +154,7 @@ BrowserID.Mocks.xhr = (function() {
         }
       }
       else if (obj.error) {
-        // Invalid result - either invalid URL, invalid GET/POST or 
+        // Invalid result - either invalid URL, invalid GET/POST or
         // invalid resultType
         obj.error({ status: result || 400 }, "errorStatus", "errorThrown");
       }
diff --git a/resources/static/test/qunit/pages/add_email_address_test.js b/resources/static/test/qunit/pages/add_email_address_test.js
index 31797401683516ebfd4328f3b4f04d53df341cfc..60028f345f6bccbecf35d462eefd17b660535044 100644
--- a/resources/static/test/qunit/pages/add_email_address_test.js
+++ b/resources/static/test/qunit/pages/add_email_address_test.js
@@ -63,45 +63,38 @@
   asyncTest("addEmailAddress with good token and site", function() {
     storage.setStagedOnBehalfOf("browserid.org");
 
-    bid.addEmailAddress("token");
-
-    setTimeout(function() {
+    bid.addEmailAddress("token", function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
       equal($(".website").text(), "browserid.org", "origin is updated");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("addEmailAddress with good token and nosite", function() {
-    bid.addEmailAddress("token");
-
-    setTimeout(function() {
+    bid.addEmailAddress("token", function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
       equal($(".siteinfo .website").text(), "", "origin is not updated");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("addEmailAddress with bad token", function() {
     xhr.useResult("invalid");
 
-    bid.addEmailAddress("token");
-    setTimeout(function() {
+    bid.addEmailAddress("token", function() {
       ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("addEmailAddress with emailForVerficationToken XHR failure", function() {
     xhr.useResult("ajaxError");
-    bid.addEmailAddress("token");
-
-    setTimeout(function() {
+    bid.addEmailAddress("token", function() {
       ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
       start();
-    }, 500);
+    });
   });
 
 }());
diff --git a/resources/static/test/qunit/pages/forgot_unit_test.js b/resources/static/test/qunit/pages/forgot_unit_test.js
index f1f5379b92b1f333ade24925e17266d6af9d9a27..47b5bb579f7dbd00a17913fdc5cffe18b41290b6 100644
--- a/resources/static/test/qunit/pages/forgot_unit_test.js
+++ b/resources/static/test/qunit/pages/forgot_unit_test.js
@@ -40,8 +40,7 @@
   var bid = BrowserID,
       network = bid.Network,
       user = bid.User,
-      xhr = bid.Mocks.xhr,
-      CHECK_DELAY = 500;
+      xhr = bid.Mocks.xhr;
 
   module("pages/forgot", {
     setup: function() {
@@ -61,13 +60,11 @@
   });
 
   function testEmailNotSent(extraTests) {
-    bid.forgot.submit();
-
-    setTimeout(function() {
+    bid.forgot.submit(function() {
       equal($(".emailsent").is(":visible"), false, "email not sent");
       if (extraTests) extraTests();
       else start();
-    }, CHECK_DELAY);
+    });
   }
 
   asyncTest("requestPasswordReset with invalid email", function() {
@@ -80,22 +77,18 @@
 
   asyncTest("requestPasswordReset with known email", function() {
     $("#email").val("registered@testuser.com");
-    bid.forgot.submit();
-
-    setTimeout(function() {
+    bid.forgot.submit(function() {
       ok($(".emailsent").is(":visible"), "email sent successfully");
       start();
-    }, CHECK_DELAY);
+    });
   });
 
   asyncTest("requestPasswordReset with known email with leading/trailing whitespace", function() {
     $("#email").val("   registered@testuser.com  ");
-    bid.forgot.submit();
-
-    setTimeout(function() {
+    bid.forgot.submit(function() {
       ok($(".emailsent").is(":visible"), "email sent successfully");
       start();
-    }, CHECK_DELAY);
+    });
   });
 
   asyncTest("requestPasswordReset with unknown email", function() {
@@ -123,22 +116,4 @@
     });
   });
 
-  asyncTest("signup with unregistered email and cancel button pressed", function() {
-    $("#email").val("unregistered@testuser.com");
-
-    bid.signUp.submit();
-
-    setTimeout(function() {
-      bid.forgot.back();
-
-      setTimeout(function() {
-        equal($(".notification:visible").length, 0, "no notifications are visible");
-        equal($(".forminputs:visible").length, 1, "form inputs are again visible");
-        equal($("#email").val(), "unregistered@testuser.com", "email address restored");
-        start();
-      }, 500);
-    }, 100);
-  });
-
-
 }());
diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js
index 8c562573a991ce04f5d02bfac43ee6e96a804945..8893a8588264e3b89f8b6d27f543178097772c98 100644
--- a/resources/static/test/qunit/pages/manage_account_unit_test.js
+++ b/resources/static/test/qunit/pages/manage_account_unit_test.js
@@ -44,8 +44,6 @@
       xhr = bid.Mocks.xhr,
       validToken = true,
       TEST_ORIGIN = "http://browserid.org",
-      TEST_DELAY = 100,
-      ERROR_DELAY = 250,
       mocks = {
         confirm: function() { return true; },
         document: { location: "" }
@@ -71,145 +69,96 @@
   });
 
   asyncTest("no email addresses are displayed if there are no children", function() {
-    xhr.useResult("noidentities");
+    xhr.useResult("no_identities");
 
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       equal($("#emailList").children().length, 0, "no children have been added");
       start();
-    }, TEST_DELAY);
-
-    
+    });
   });
 
   asyncTest("email addresses added if there are children", function() {
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       equal($("#emailList").children().length, 1, "there has been one child added");
       start();
-    }, TEST_DELAY);
-
-    
+    });
   });
 
   asyncTest("sync XHR error on startup", function() {
     xhr.useResult("ajaxError");
 
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       equal($("#error").is(":visible"), true, "error message is visible on XHR error");
       start();
-    }, ERROR_DELAY);
-
-    
+    });
   });
 
   asyncTest("removeEmail with multiple emails", function() {
     // start with multiple addresses.
     xhr.useResult("multiple");
 
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       // switch to a single address return on the sync.
       xhr.useResult("valid");
-      bid.manageAccount.removeEmail("testuser@testuser.com");
-
-      setTimeout(function() {
+      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
         equal($("#emailList").children().length, 1, "after removing an email, only one remains");
         start();
-      }, TEST_DELAY);
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
   asyncTest("removeEmail with multiple emails and XHR error", function() {
     // start with multiple addresses.
     xhr.useResult("multiple");
 
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       xhr.useResult("ajaxError");
-      bid.manageAccount.removeEmail("testuser@testuser.com");
-
-      setTimeout(function() {
+      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
         equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
-      }, ERROR_DELAY);
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
   asyncTest("removeEmail with single email cancels account", function() {
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
-      bid.manageAccount.removeEmail("testuser@testuser.com");
-
-      setTimeout(function() {
+    bid.manageAccount(mocks, function() {
+      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
         equal(mocks.document.location, "/", "redirection happened");
         start();
-      }, TEST_DELAY);
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
   asyncTest("removeEmail with single email cancels account and XHR error", function() {
     xhr.useResult("valid");
 
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       xhr.useResult("ajaxError");
 
-      bid.manageAccount.removeEmail("testuser@testuser.com");
-
-      setTimeout(function() {
+      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
         equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
-      }, ERROR_DELAY);
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
   asyncTest("cancelAccount", function() {
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
-      bid.manageAccount.cancelAccount();
-
-      setTimeout(function() {
+    bid.manageAccount(mocks, function() {
+      bid.manageAccount.cancelAccount(function() {
         equal(mocks.document.location, "/", "redirection happened");
         start();
-      }, TEST_DELAY);
-
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
   asyncTest("cancelAccount with XHR error", function() {
-    bid.manageAccount(mocks);
-
-    setTimeout(function() {
+    bid.manageAccount(mocks, function() {
       xhr.useResult("ajaxError");
-      bid.manageAccount.cancelAccount();
-
-      setTimeout(function() {
+      bid.manageAccount.cancelAccount(function() {
         equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
-      }, ERROR_DELAY);
-    }, TEST_DELAY);
-
-    
+      });
+    });
   });
 
 }());
diff --git a/resources/static/test/qunit/pages/signin_unit_test.js b/resources/static/test/qunit/pages/signin_unit_test.js
index eb0a0d6330f0f36349e26e739660e8af6da7f827..50a785b9f1aae86bb53994364d144891e36822fb 100644
--- a/resources/static/test/qunit/pages/signin_unit_test.js
+++ b/resources/static/test/qunit/pages/signin_unit_test.js
@@ -41,7 +41,6 @@
       network = bid.Network,
       user = bid.User,
       xhr = bid.Mocks.xhr,
-      CHECK_DELAY = 500,
       docMock = {
         location: "signin"
       }
@@ -65,37 +64,31 @@
   });
 
   function testUserNotSignedIn(extraTests) {
-    bid.signIn.submit();
-
-    setTimeout(function() {
+    bid.signIn.submit(function() {
       equal(docMock.location, "signin", "user not signed in");
       if (extraTests) extraTests();
-      else start();
-    }, 100);
+      start();
+    });
   }
 
   asyncTest("signin with valid email and password", function() {
     $("#email").val("registered@testuser.com");
     $("#password").val("password");
 
-    bid.signIn.submit();
-
-    setTimeout(function() {
+    bid.signIn.submit(function() {
       equal(docMock.location, "/", "user signed in, page redirected");
       start();
-    }, 100);
+    });
   });
 
   asyncTest("signin with valid email with leading/trailing whitespace and password", function() {
     $("#email").val("  registered@testuser.com  ");
     $("#password").val("password");
 
-    bid.signIn.submit();
-
-    setTimeout(function() {
+    bid.signIn.submit(function() {
       equal(docMock.location, "/", "user signed in, page redirected");
       start();
-    }, 100);
+    });
   });
 
   asyncTest("signin with missing email", function() {
@@ -127,10 +120,7 @@
     $("#password").val("password");
 
     testUserNotSignedIn(function() {
-      setTimeout(function() {
         equal($("#error").is(":visible"), true, "error is visible");
-        start();
-      }, 500);
     });
   });
 
diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js
index 62a70115107e207a15b36dadefe10adb4bd80ea1..24bf65256377ceb5371a54bcbe655530f3682324 100644
--- a/resources/static/test/qunit/pages/signup_unit_test.js
+++ b/resources/static/test/qunit/pages/signup_unit_test.js
@@ -41,7 +41,6 @@
       network = bid.Network,
       user = bid.User,
       xhr = bid.Mocks.xhr,
-      CHECK_DELAY = 500,
       testOrigin = "http://browserid.org";
 
   module("pages/signup", {
@@ -65,35 +64,29 @@
   });
 
   function testNoticeNotVisible(extraTests) {
-    bid.signUp.submit();
-
-    setTimeout(function() {
+    bid.signUp.submit(function() {
       equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible");
       if(extraTests) extraTests();
-      else start();
-    }, CHECK_DELAY);
+      start();
+    });
   }
 
   asyncTest("signup with valid unregistered email", function() {
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit();
-
-    setTimeout(function() {
+    bid.signUp.submit(function() {
       equal($(".emailsent").is(":visible"), true, "email sent, notice visible");
       start();
-    }, CHECK_DELAY);
+    });
   });
 
   asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() {
     $("#email").val(" unregistered@testuser.com ");
 
-    bid.signUp.submit();
-
-    setTimeout(function() {
+    bid.signUp.submit(function() {
       equal($(".emailsent").is(":visible"), true, "email sent, notice visible");
       start();
-    }, CHECK_DELAY);
+    });
   });
 
   asyncTest("signup with valid registered email", function() {
@@ -122,25 +115,20 @@
 
     testNoticeNotVisible(function() {
       equal($("#error").is(":visible"), true, "error message displayed");
-      start();
     });
   });
 
   asyncTest("signup with unregistered email and cancel button pressed", function() {
     $("#email").val("unregistered@testuser.com");
 
-    bid.signUp.submit();
-
-    setTimeout(function() {
-      bid.signUp.back();
-
-      setTimeout(function() {
+    bid.signUp.submit(function() {
+      bid.signUp.back(function() {
         equal($(".notification:visible").length, 0, "no notifications are visible");
         equal($(".forminputs:visible").length, 1, "form inputs are again visible");
         equal($("#email").val(), "unregistered@testuser.com", "email address restored");
         start();
-      }, 500);
-    }, 100);
+      });
+    });
   });
 
 }());
diff --git a/resources/static/test/qunit/pages/verify_email_address_test.js b/resources/static/test/qunit/pages/verify_email_address_test.js
index 95231118bb8d6508c6b8f448f00eccb910fb92df..ac306d1eaa9f9a39408ea6452142c0e466f09d4a 100644
--- a/resources/static/test/qunit/pages/verify_email_address_test.js
+++ b/resources/static/test/qunit/pages/verify_email_address_test.js
@@ -64,80 +64,65 @@
   asyncTest("verifyEmailAddress with good token and site", function() {
     storage.setStagedOnBehalfOf("browserid.org");
 
-    bid.verifyEmailAddress("token");
-
-    setTimeout(function() {
+    bid.verifyEmailAddress("token", function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
       equal($(".website").text(), "browserid.org", "origin is updated");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("verifyEmailAddress with good token and nosite", function() {
     $(".siteinfo").hide();
     storage.setStagedOnBehalfOf("");
 
-    bid.verifyEmailAddress("token");
-
-
-    setTimeout(function() {
+    bid.verifyEmailAddress("token", function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
       equal($(".siteinfo .website").text(), "", "origin is not updated");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("verifyEmailAddress with bad token", function() {
     xhr.useResult("invalid");
 
-    bid.verifyEmailAddress("token");
-    setTimeout(function() {
+    bid.verifyEmailAddress("token", function() {
       ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("verifyEmailAddress with emailForVerficationToken XHR failure", function() {
     xhr.useResult("ajaxError");
-    bid.verifyEmailAddress("token");
-
-    setTimeout(function() {
+    bid.verifyEmailAddress("token", function() {
       ok($("#error").is(":visible"), "cannot communicate box is visible");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("submit with good token, both passwords", function() {
-    bid.verifyEmailAddress("token");
-
-
-    $("#password").val("password");
-    $("#vpassword").val("password");
-
-    bid.verifyEmailAddress.submit();
-
-    setTimeout(function() {
-      equal($("#congrats").is(":visible"), true, "congrats is visible, we are complete");
-      start();
-    }, 500);
+    bid.verifyEmailAddress("token", function() {
+      $("#password").val("password");
+      $("#vpassword").val("password");
+
+      bid.verifyEmailAddress.submit(function() {
+        equal($("#congrats").is(":visible"), true, "congrats is visible, we are complete");
+        start();
+      });
+    });
   });
 
   asyncTest("submit with good token, missing password", function() {
-    bid.verifyEmailAddress("token");
-
-
-    $("#password").val("");
-    $("#vpassword").val("password");
-
-    bid.verifyEmailAddress.submit();
-
-    setTimeout(function() {
-      equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password");
-      start();
-    }, 500);
-    
+    bid.verifyEmailAddress("token", function() {
+      $("#password").val("");
+      $("#vpassword").val("password");
+
+      bid.verifyEmailAddress.submit(function() {
+        equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password");
+        start();
+      });
+    });
   });
 
   asyncTest("submit with good token, missing verification password", function() {
@@ -147,13 +132,11 @@
     $("#password").val("password");
     $("#vpassword").val("");
 
-    bid.verifyEmailAddress.submit();
-
-    setTimeout(function() {
+    bid.verifyEmailAddress.submit(function() {
       equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password");
       start();
-    }, 500);
-    
+    });
+
   });
 
   asyncTest("submit with good token, different passwords", function() {
@@ -162,12 +145,10 @@
     $("#password").val("password");
     $("#vpassword").val("pass");
 
-    bid.verifyEmailAddress.submit();
-
-    setTimeout(function() {
+    bid.verifyEmailAddress.submit(function() {
       equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords");
       start();
-    }, 500);
-    
+    });
+
   });
 }());
diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js
index b2790de56a0a5981215e84b463107f3707530a86..f0c19b2a7b03f5e63738e3514cfb75190e874d8e 100644
--- a/resources/static/test/qunit/shared/user_unit_test.js
+++ b/resources/static/test/qunit/shared/user_unit_test.js
@@ -253,7 +253,7 @@ var jwcert = require("./jwcert");
       lib.cancelUserValidation();
       ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared when validation cancelled");
       start();
-    }, 1000);
+    }, 500);
   });
 
   asyncTest("verifyUser with a good token", function() {
@@ -588,7 +588,7 @@ var jwcert = require("./jwcert");
       lib.cancelUserValidation();
       ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared when validation cancelled");
       start();
-    }, 1000);
+    }, 500);
   });
 
   asyncTest("verifyEmail with a good token", function() {
@@ -840,7 +840,7 @@ var jwcert = require("./jwcert");
       }, failure("syncEmails failure"));
     }, failure("authenticate failure"));
 
-    
+
   });
 
   asyncTest("logoutUser with XHR failure", function(onSuccess) {
@@ -860,7 +860,7 @@ var jwcert = require("./jwcert");
       }, failure("syncEmails failure"));
     }, failure("authenticate failure"));
 
-    
+
   });
 
   asyncTest("cancelUser", function(onSuccess) {
@@ -870,7 +870,7 @@ var jwcert = require("./jwcert");
       start();
     });
 
-    
+
   });
 
   asyncTest("cancelUser with XHR failure", function(onSuccess) {
@@ -883,7 +883,7 @@ var jwcert = require("./jwcert");
       start();
     });
 
-    
+
   });
 
   asyncTest("getPersistentSigninAssertion with invalid login", function() {
@@ -903,7 +903,7 @@ var jwcert = require("./jwcert");
       });
     });
 
-    
+
   });
 
   asyncTest("getPersistentSigninAssertion with valid login with remember set to true but no email", function() {
@@ -919,7 +919,7 @@ var jwcert = require("./jwcert");
       start();
     });
 
-    
+
   });
 
   asyncTest("getPersistentSigninAssertion with valid login with email and remember set to false", function() {
@@ -940,7 +940,7 @@ var jwcert = require("./jwcert");
       });
     });
 
-    
+
   });
 
   asyncTest("getPersistentSigninAssertion with valid login, email, and remember set to true", function() {
@@ -961,7 +961,7 @@ var jwcert = require("./jwcert");
       });
     });
 
-    
+
   });
 
   asyncTest("getPersistentSigninAssertion with XHR failure", function() {
@@ -984,7 +984,7 @@ var jwcert = require("./jwcert");
       });
     });
 
-    
+
   });
 
   asyncTest("clearPersistentSignin with invalid login", function() {
@@ -998,7 +998,7 @@ var jwcert = require("./jwcert");
       start();
     });
 
-    
+
   });
 
   asyncTest("clearPersistentSignin with valid login with remember set to true", function() {
@@ -1014,6 +1014,6 @@ var jwcert = require("./jwcert");
       start();
     });
 
-    
+
   });
 }());
diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e76179687875a04e4c794cb2ede4357cdea6066
--- /dev/null
+++ b/resources/static/test/qunit/testHelpers/helpers.js
@@ -0,0 +1,55 @@
+(function() {
+  var bid = BrowserID,
+      mediator = bid.Mediator,
+      network = bid.Network,
+      storage = bid.Storage,
+      xhr = bid.Mocks.xhr,
+      registrations = [];
+      calls = {};
+
+  function register(message, cb) {
+    registrations.push(mediator.subscribe(message, function(msg, info) {
+      if(calls[msg]) {
+        throw msg + " triggered more than once";
+      }
+      calls[msg] = true;
+
+      cb(msg, info);
+    }));
+  }
+
+  function unregisterAll() {
+    var registration;
+    for(var i = 0, registration; registration = registrations[i]; ++i) {
+      mediator.unsubscribe(registration);
+    }
+    registrations = [];
+    calls = {};
+  }
+
+  BrowserID.TestHelpers = {
+    setup: function() {
+      network.setXHR(xhr);
+      xhr.useResult("valid");
+      storage.clear();
+
+      var el = $("#controller_head");
+      el.find("#formWrap .contents").html("");
+      el.find("#wait .contents").html("");
+      $("#error").html("<div class='contents'></div>").hide();
+
+      unregisterAll();
+      mediator.reset();
+    },
+
+    teardown: function() {
+      unregisterAll();
+      mediator.reset();
+      network.setXHR($);
+      storage.clear();
+      $("#error").html("<div class='contents'></div>").hide();
+    },
+
+    register: register
+  };
+}());
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
index 6873159afed75c94a1ed99549570c79f19f4ad85..0f39976e198655dc7ab6e172065f97c6f7f35c08 100644
--- a/resources/views/index.ejs
+++ b/resources/views/index.ejs
@@ -1,11 +1,11 @@
   <div id="content" style="display:none" class="display_auth">
       <div id="manage">
           <h1 class="serif">Account Manager</h1>
-          <div class="edit cf">
+          <div class="buttonrow cf">
               <strong>Your Email Addresses</strong>
 
-              <a id="manageAccounts" href="#">edit</a>
-              <a id="cancelManage" href="#">done</a>
+              <button id="manageAccounts" href="#">edit</button>
+              <button id="cancelManage" href="#">done</button>
           </div>
           <ul id="emailList">
           </ul>
@@ -21,8 +21,8 @@
 
           <p>Connect with <em>BrowserID</em>, the safest &amp; easiest way to sign in.</p>
           <p>
-            <a class="granted info" href="/about">Take the tour</a> 
-            <span class="require-js">or 
+            <a class="granted info" href="/about">Take the tour</a>
+            <span class="require-js">or
               <a href="/signup" class="button granted create">sign up</a>
             </span>
           </p>