From 77c7f445707eab90f22640077834d38daace77d7 Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Sun, 20 Nov 2011 21:58:45 +0000
Subject: [PATCH] JavascriptMVC is now removed as a dependency.

* StealJS is still the script loader.
* Putting OpenAjax.hub into lib.
* Adding a Class/SubClass mechanism.
* Converting all controllers to be Subclassed from Modules.PageModule.
* All Modules are registered first, then created/started on demand.\
---
 .../{addemail_controller.js => addemail.js}   |   8 +-
 ...enticate_controller.js => authenticate.js} |   8 +-
 ...ion_controller.js => checkregistration.js} |   8 +-
 .../{dialog_controller.js => dialog.js}       |  48 +++--
 ...ssword_controller.js => forgotpassword.js} |   8 +-
 .../{page_controller.js => page.js}           |  21 +-
 .../{pickemail_controller.js => pickemail.js} |  14 +-
 ..._email_controller.js => required_email.js} |   8 +-
 resources/static/dialog/dialog.js             |  51 +++--
 resources/static/lib/module.js                |   4 +-
 resources/static/lib/openajax.js              | 201 ++++++++++++++++++
 resources/static/shared/class.js              |  56 +++++
 resources/static/shared/mediator.js           |  46 ++++
 ...ler_unit_test.js => addemail_unit_test.js} |  23 +-
 ...unit_test.js => authenticate_unit_test.js} |  15 +-
 ...test.js => checkregistration_unit_test.js} |  20 +-
 ...oller_unit_test.js => dialog_unit_test.js} |  24 +--
 ...it_test.js => forgotpassword_unit_test.js} |  12 +-
 ...troller_unit_test.js => page_unit_test.js} |  55 +++--
 ...er_unit_test.js => pickemail_unit_test.js} |  10 +-
 ...it_test.js => required_email_unit_test.js} |  58 ++---
 .../test/qunit/shared/class_unit_test.js      | 134 ++++++++++++
 22 files changed, 669 insertions(+), 163 deletions(-)
 rename resources/static/dialog/controllers/{addemail_controller.js => addemail.js} (94%)
 rename resources/static/dialog/controllers/{authenticate_controller.js => authenticate.js} (96%)
 rename resources/static/dialog/controllers/{checkregistration_controller.js => checkregistration.js} (93%)
 rename resources/static/dialog/controllers/{dialog_controller.js => dialog.js} (90%)
 rename resources/static/dialog/controllers/{forgotpassword_controller.js => forgotpassword.js} (93%)
 rename resources/static/dialog/controllers/{page_controller.js => page.js} (93%)
 rename resources/static/dialog/controllers/{pickemail_controller.js => pickemail.js} (92%)
 rename resources/static/dialog/controllers/{required_email_controller.js => required_email.js} (97%)
 create mode 100644 resources/static/lib/openajax.js
 create mode 100644 resources/static/shared/class.js
 create mode 100644 resources/static/shared/mediator.js
 rename resources/static/test/qunit/controllers/{addemail_controller_unit_test.js => addemail_unit_test.js} (91%)
 rename resources/static/test/qunit/controllers/{authenticate_controller_unit_test.js => authenticate_unit_test.js} (95%)
 rename resources/static/test/qunit/controllers/{checkregistration_controller_unit_test.js => checkregistration_unit_test.js} (88%)
 rename resources/static/test/qunit/controllers/{dialog_controller_unit_test.js => dialog_unit_test.js} (92%)
 rename resources/static/test/qunit/controllers/{forgotpassword_controller_unit_test.js => forgotpassword_unit_test.js} (92%)
 rename resources/static/test/qunit/controllers/{page_controller_unit_test.js => page_unit_test.js} (89%)
 rename resources/static/test/qunit/controllers/{pickemail_controller_unit_test.js => pickemail_unit_test.js} (97%)
 rename resources/static/test/qunit/controllers/{required_email_controller_unit_test.js => required_email_unit_test.js} (91%)
 create mode 100644 resources/static/test/qunit/shared/class_unit_test.js

diff --git a/resources/static/dialog/controllers/addemail_controller.js b/resources/static/dialog/controllers/addemail.js
similarity index 94%
rename from resources/static/dialog/controllers/addemail_controller.js
rename to resources/static/dialog/controllers/addemail.js
index 12b37b9e5..8ebdd41e2 100644
--- a/resources/static/dialog/controllers/addemail_controller.js
+++ b/resources/static/dialog/controllers/addemail.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.AddEmail = (function() {
   "use strict";
 
   var bid = BrowserID,
@@ -66,18 +66,20 @@
     this.close("cancel_add_email");
   }
 
-  PageController.extend("Addemail", {}, {
+  var AddEmail = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this;
 
       self.renderDialog("addemail");
 
       self.bind("#cancelNewEmail", "click", cancelAddEmail);
-      self._super();
+      AddEmail.sc.start.call(self, options);
     },
     submit: addEmail,
     addEmail: addEmail,
     cancelAddEmail: cancelAddEmail
   });
 
+  return AddEmail;
+
 }());
diff --git a/resources/static/dialog/controllers/authenticate_controller.js b/resources/static/dialog/controllers/authenticate.js
similarity index 96%
rename from resources/static/dialog/controllers/authenticate_controller.js
rename to resources/static/dialog/controllers/authenticate.js
index 31e8a1145..970a9a71f 100644
--- a/resources/static/dialog/controllers/authenticate_controller.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.Authenticate = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
@@ -155,7 +155,7 @@
     }
   }
 
-  PageController.extend("Authenticate", {}, {
+  var Authenticate = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this;
       self.renderDialog("authenticate", {
@@ -172,7 +172,7 @@
       self.bind("#email", "keyup", emailKeyUp);
       self.bind("#forgotPassword", "click", forgotPassword);
 
-      self._super();
+      Authenticate.sc.start.call(self, options);
     },
 
     checkEmail: checkEmail,
@@ -181,4 +181,6 @@
     forgotPassword: forgotPassword
   });
 
+  return Authenticate;
+
 }());
diff --git a/resources/static/dialog/controllers/checkregistration_controller.js b/resources/static/dialog/controllers/checkregistration.js
similarity index 93%
rename from resources/static/dialog/controllers/checkregistration_controller.js
rename to resources/static/dialog/controllers/checkregistration.js
index 78038e683..437f9fc1d 100644
--- a/resources/static/dialog/controllers/checkregistration_controller.js
+++ b/resources/static/dialog/controllers/checkregistration.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.CheckRegistration = (function() {
   "use strict";
 
   var bid = BrowserID,
@@ -42,7 +42,7 @@
       dom = bid.DOM,
       errors = bid.Errors;
 
-  PageController.extend("Checkregistration", {}, {
+  var CheckRegistration = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this;
       self.renderWait("confirmemail", {
@@ -54,7 +54,7 @@
 
       self.bind("#back", "click", self.cancel);
 
-      self._super();
+      CheckRegistration.sc.start.call(self, options);
     },
 
     startCheck: function() {
@@ -78,6 +78,6 @@
 
   });
 
-
+  return CheckRegistration;
 
 }());
diff --git a/resources/static/dialog/controllers/dialog_controller.js b/resources/static/dialog/controllers/dialog.js
similarity index 90%
rename from resources/static/dialog/controllers/dialog_controller.js
rename to resources/static/dialog/controllers/dialog.js
index 82c2e7688..7c548e0ea 100644
--- a/resources/static/dialog/controllers/dialog_controller.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -1,5 +1,5 @@
 /*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*global setupChannel:true, BrowserID: true, PageController: true, OpenAjax: true */
+/*global setupChannel:true, BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -35,11 +35,8 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-//
-// a JMVC controller for the browserid dialog
-//
 
-(function() {
+BrowserID.Modules.Dialog = (function() {
   "use strict";
 
   var bid = BrowserID,
@@ -49,26 +46,36 @@
       offline = false,
       win = window,
       subscriptions = [],
-      hub = OpenAjax.hub;
+      mediator = bid.Mediator,
+      serviceManager = bid.module,
+      runningService;
 
   function subscribe(message, cb) {
-     subscriptions.push(hub.subscribe(message, cb));
+     subscriptions.push(mediator.subscribe(message, cb));
+  }
+
+  function startService(name, options) {
+    // Only one service outside of the main dialog allowed.
+    if(runningService) {
+      serviceManager.stop(runningService);
+    }
+    runningService = name;
+    return serviceManager.start(name, options);
   }
 
   function createCheckRegistrationController(email, verifier, message) {
     this.confirmEmail = email;
 
-    var controller = this.element.checkregistration({
+    var controller = startService("check_registration", {
       email: email,
       verifier: verifier,
       verificationMessage: message
-    }).controller("checkregistration");  // specify the name of the controller
-                                         // or else the dialog controller is returned.
+    }); 
     controller.startCheck();
   }
 
-  PageController.extend("Dialog", {}, {
-      init: function(el, options) {
+  var Dialog = bid.Modules.PageModule.extend({
+      init: function(options) {
         offline = false;
 
         options = options || {};
@@ -80,7 +87,7 @@
         var self=this;
 
         self.domEvents = [];
-        self._super();
+        Dialog.sc.init.call(self, options);
 
         // keep track of where we are and what we do on success and error
         self.onsuccess = null;
@@ -100,10 +107,10 @@
         var subscription;
 
         while(subscription = subscriptions.pop()) {
-          hub.unsubscribe(subscription);
+          mediator.unsubscribe(subscription);
         }
 
-        this._super();
+        Dialog.sc.destroy.call(this);
       },
 
       getVerifiedEmail: function(origin_url, onsuccess, onerror) {
@@ -269,7 +276,7 @@
       doPickEmail: function() {
         var self=this;
         self.returnFromStageCancel = self.doPickEmail.bind(self);
-        self.element.pickemail({
+        startService("pick_email", {
           // XXX ideal is to get rid of this and have a User function
           // that takes care of getting email addresses AND the last used email
           // for this site.
@@ -279,7 +286,7 @@
       },
 
       doAddEmail: function() {
-        this.element.addemail({});
+        startService("add_email", {});
       },
 
       doAuthenticate: function(info) {
@@ -288,17 +295,17 @@
         // Save this off in case the user forgets their password or goes to add 
         // a new password but has to cancel.
         self.returnFromStageCancel = self.doAuthenticate.bind(self, info);
-        self.element.authenticate(info);
+        startService("authenticate", info);
       },
 
       doAuthenticateWithRequiredEmail: function(info) {
         var self=this;
         self.returnFromStageCancel = self.doAuthenticateWithRequiredEmail.bind(self, info);
-        self.element.requiredemail(info);
+        startService("required_email", info);
       },
 
       doForgotPassword: function(email) {
-        this.element.forgotpassword({
+        startService("forgot_password", {
           email: email
         });
       },
@@ -355,5 +362,6 @@
 
   });
 
+  return Dialog;
 
 }());
diff --git a/resources/static/dialog/controllers/forgotpassword_controller.js b/resources/static/dialog/controllers/forgotpassword.js
similarity index 93%
rename from resources/static/dialog/controllers/forgotpassword_controller.js
rename to resources/static/dialog/controllers/forgotpassword.js
index d1bdda2d7..25bb28fef 100644
--- a/resources/static/dialog/controllers/forgotpassword_controller.js
+++ b/resources/static/dialog/controllers/forgotpassword.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.ForgotPassword = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
@@ -61,7 +61,7 @@
     this.close("cancel_forgot_password");
   }
 
-  PageController.extend("Forgotpassword", {}, {
+  var ForgotPassword = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this;
       self.email = options.email;
@@ -71,7 +71,7 @@
 
       self.bind("#cancel_forgot_password", "click", cancelResetPassword);
 
-      self._super();
+      ForgotPassword.sc.start.call(self, options);
     },
 
     submit: resetPassword,
@@ -79,4 +79,6 @@
     cancelResetPassword: cancelResetPassword
   });
 
+  return ForgotPassword;
+
 }());
diff --git a/resources/static/dialog/controllers/page_controller.js b/resources/static/dialog/controllers/page.js
similarity index 93%
rename from resources/static/dialog/controllers/page_controller.js
rename to resources/static/dialog/controllers/page.js
index 0c66dfe58..0aada5d79 100644
--- a/resources/static/dialog/controllers/page_controller.js
+++ b/resources/static/dialog/controllers/page.js
@@ -34,13 +34,15 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules = BrowserID.Modules || {};
+BrowserID.Modules.PageModule = (function() {
 "use strict";
 
   var ANIMATION_TIME = 250,
       bid = BrowserID,
       dom = bid.DOM,
-      screens = bid.Screens;
+      screens = bid.Screens,
+      mediator = bid.Mediator;
 
    function onSubmit(event) {
      event.stopPropagation();
@@ -52,10 +54,8 @@
      return false;
    }
 
-
-  $.Controller.extend("PageController", {
-    }, {
-    init: function(el, options) {
+  var PageController = BrowserID.Class({
+    init: function(options) {
       options = options || {};
 
       var self=this;
@@ -73,8 +73,6 @@
       if(options.errorTemplate) {
         self.renderError(options.errorTemplate, options.errorVars);
       }
-
-      self.start(options);
     },
 
     start: function() {
@@ -91,7 +89,6 @@
 
     destroy: function() {
       this.stop();
-      this._super();
     },
 
     bind: function(target, type, callback, context) {
@@ -157,6 +154,10 @@
       }
     },
 
+    publish: function(message, data) {
+      mediator.publish(message, data);
+    },
+
     /**
      * Get a curried function to an error dialog.
      * @method getErrorDialog
@@ -173,4 +174,6 @@
     }
   });
 
+  return PageController;
+
 }());
diff --git a/resources/static/dialog/controllers/pickemail_controller.js b/resources/static/dialog/controllers/pickemail.js
similarity index 92%
rename from resources/static/dialog/controllers/pickemail_controller.js
rename to resources/static/dialog/controllers/pickemail.js
index d7c96093b..bec32eedb 100644
--- a/resources/static/dialog/controllers/pickemail_controller.js
+++ b/resources/static/dialog/controllers/pickemail.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.PickEmail = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
@@ -55,12 +55,12 @@
     cancelEvent(event);
 
     var self=this;
-    if (!self.find("input[type=radio]:checked").length) {
+    if (!dom.getElements("input[type=radio]:checked").length) {
       // If none are already checked, select the first one.
-      self.find('input[type=radio]').eq(0).attr('checked', true);
+      dom.setAttr('input[type=radio]:eq(0)', 'checked', true);
     }
     // focus whichever is checked.
-    self.find("input[type=radio]:checked").focus();
+    dom.focus("input[type=radio]:checked");
     self.submit = signIn;
   }
 
@@ -100,7 +100,7 @@
     }
   }
 
-  PageController.extend("Pickemail", {}, {
+  var PickEmail = bid.Modules.PageModule.extend({
     start: function(options) {  
       var origin = user.getOrigin(),
           self=this;
@@ -128,7 +128,7 @@
 
       self.bind("#useNewEmail", "click", addEmail);
 
-      self._super();
+      PickEmail.sc.start.call(self, options);
 
       pickEmailState.call(self);
     },
@@ -137,4 +137,6 @@
     addEmail: addEmail
   });
 
+  return PickEmail;
+
 }());
diff --git a/resources/static/dialog/controllers/required_email_controller.js b/resources/static/dialog/controllers/required_email.js
similarity index 97%
rename from resources/static/dialog/controllers/required_email_controller.js
rename to resources/static/dialog/controllers/required_email.js
index eb49e0c32..f84368eac 100644
--- a/resources/static/dialog/controllers/required_email_controller.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-(function() {
+BrowserID.Modules.RequiredEmail = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
@@ -112,7 +112,7 @@
     this.close("cancel");
   }
 
-  PageController.extend("Requiredemail", {}, {
+  var RequiredEmail = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this,
           email = options.email || "",
@@ -153,7 +153,7 @@
         self.bind("#cancel", "click", cancel);
       }
 
-      self._super();
+      RequiredEmail.sc.start.call(self, options);
     },
 
     signIn: signIn,
@@ -162,4 +162,6 @@
     cancel: cancel
   });
 
+  return RequiredEmail;
+
 }());
diff --git a/resources/static/dialog/dialog.js b/resources/static/dialog/dialog.js
index 6bbaaeb03..c0f655bc3 100644
--- a/resources/static/dialog/dialog.js
+++ b/resources/static/dialog/dialog.js
@@ -39,21 +39,21 @@ window.console = window.console || {
   log: function() {}
 };
 
-steal
-  .plugins(
-              'jquery/controller',			// a widget factory
-              'jquery/controller/subscribe')	// subscribe to OpenAjax.hub
-
-	.resources(  'channel')
-  .then(
+steal.then(
+               'resources/channel',
+               '../lib/jquery-1.6.2.min.js',
                '../lib/jschannel',
                '../lib/base64',
                '../lib/underscore-min',
                '../lib/vepbundle',
                '../lib/ejs',
                '../shared/browserid',
+               '../lib/openajax',
                '../lib/dom-jquery',
+               '../lib/module',
 
+               '../shared/mediator',
+               '../shared/class',
                '../shared/storage',
                '../shared/templates',
                '../shared/renderer',
@@ -68,21 +68,34 @@ steal
                '../shared/browserid-extensions',
                '../shared/wait-messages',
                '../shared/helpers',
-               'resources/helpers'
-               )
 
-	.controllers('page',
-               'dialog',
-               'authenticate',
-               'forgotpassword',
-               'checkregistration',
-               'pickemail',
-               'addemail',
-               'required_email'
+               'resources/helpers',
+
+	             'controllers/page',
+               'controllers/dialog',
+               'controllers/authenticate',
+               'controllers/forgotpassword',
+               'controllers/checkregistration',
+               'controllers/pickemail',
+               'controllers/addemail',
+               'controllers/required_email'
                )					// loads files in controllers folder
 
   .then(function() {
     $(function() {
-      $('body').dialog().show();
+      var bid = BrowserID,
+          moduleManager = bid.module,
+          modules = bid.Modules;
+      
+      moduleManager.register("dialog", modules.Dialog);
+      moduleManager.register("add_email", modules.AddEmail);
+      moduleManager.register("authenticate", modules.Authenticate);
+      moduleManager.register("check_registration", modules.CheckRegistration);
+      moduleManager.register("forgot_password", modules.ForgotPassword);
+      moduleManager.register("pick_email", modules.PickEmail);
+      moduleManager.register("required_email", modules.RequiredEmail);
+
+      moduleManager.start("dialog");
+
     });
-  });						// adds views to be added to build
+  });						
diff --git a/resources/static/lib/module.js b/resources/static/lib/module.js
index e9b06f406..69b0d510c 100644
--- a/resources/static/lib/module.js
+++ b/resources/static/lib/module.js
@@ -51,14 +51,14 @@ BrowserID.module = (function() {
 
         module = new constr();
         created[service] = module;
-        module.init(config);
+        module.init(config || {});
       }
       else {
         throw "module not registered for " + service;
       }
     }
 
-    module.start(data);
+    module.start(data || {});
     running[service] = module;
 
     return module;
diff --git a/resources/static/lib/openajax.js b/resources/static/lib/openajax.js
new file mode 100644
index 000000000..9160f49d2
--- /dev/null
+++ b/resources/static/lib/openajax.js
@@ -0,0 +1,201 @@
+//@steal-clean
+/*******************************************************************************
+ * OpenAjax.js
+ *
+ * Reference implementation of the OpenAjax Hub, as specified by OpenAjax Alliance.
+ * Specification is under development at: 
+ *
+ *   http://www.openajax.org/member/wiki/OpenAjax_Hub_Specification
+ *
+ * Copyright 2006-2008 OpenAjax Alliance
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not 
+ * use this file except in compliance with the License. You may obtain a copy 
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0 . Unless 
+ * required by applicable law or agreed to in writing, software distributed 
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
+ * CONDITIONS OF ANY KIND, either express or implied. See the License for the 
+ * specific language governing permissions and limitations under the License.
+ *
+ ******************************************************************************/
+(function() {
+  // prevent re-definition of the OpenAjax object
+  if(!window["OpenAjax"]){
+    /**
+     * @class OpenAjax
+     * Use OpenAjax.hub to publish and subscribe to messages.
+     */
+      OpenAjax = new function(){
+      var t = true;
+      var f = false;
+      var g = window;
+      var ooh = "org.openajax.hub.";
+
+      var h = {};
+      this.hub = h;
+      h.implementer = "http://openajax.org";
+      h.implVersion = "1.0";
+      h.specVersion = "1.0";
+      h.implExtraData = {};
+      var libs = {};
+      h.libraries = libs;
+
+      h.registerLibrary = function(prefix, nsURL, version, extra){
+        libs[prefix] = {
+          prefix: prefix,
+          namespaceURI: nsURL,
+          version: version,
+          extraData: extra 
+        };
+        this.publish(ooh+"registerLibrary", libs[prefix]);
+      }
+      h.unregisterLibrary = function(prefix){
+        this.publish(ooh+"unregisterLibrary", libs[prefix]);
+        delete libs[prefix];
+      }
+
+      h._subscriptions = { c:{}, s:[] };
+      h._cleanup = [];
+      h._subIndex = 0;
+      h._pubDepth = 0;
+
+      h.subscribe = function(name, callback, scope, subscriberData, filter)			
+      {
+        if(!scope){
+          scope = window;
+        }
+        var handle = name + "." + this._subIndex;
+        var sub = { scope: scope, cb: callback, fcb: filter, data: subscriberData, sid: this._subIndex++, hdl: handle };
+        var path = name.split(".");
+        this._subscribe(this._subscriptions, path, 0, sub);
+        return handle;
+      }
+
+      h.publish = function(name, message)		
+      {
+        var path = name.split(".");
+        this._pubDepth++;
+        this._publish(this._subscriptions, path, 0, name, message);
+        this._pubDepth--;
+        if((this._cleanup.length > 0) && (this._pubDepth == 0)) {
+          for(var i = 0; i < this._cleanup.length; i++) 
+            this.unsubscribe(this._cleanup[i].hdl);
+          delete(this._cleanup);
+          this._cleanup = [];
+        }
+      }
+
+      h.unsubscribe = function(sub) 
+      {
+        var path = sub.split(".");
+        var sid = path.pop();
+        this._unsubscribe(this._subscriptions, path, 0, sid);
+      }
+      
+      h._subscribe = function(tree, path, index, sub) 
+      {
+        var token = path[index];
+        if(index == path.length) 	
+          tree.s.push(sub);
+        else { 
+          if(typeof tree.c == "undefined")
+             tree.c = {};
+          if(typeof tree.c[token] == "undefined") {
+            tree.c[token] = { c: {}, s: [] }; 
+            this._subscribe(tree.c[token], path, index + 1, sub);
+          }
+          else 
+            this._subscribe( tree.c[token], path, index + 1, sub);
+        }
+      }
+
+      h._publish = function(tree, path, index, name, msg, pcb, pcid) {
+        if(typeof tree != "undefined") {
+          var node;
+          if(index == path.length) {
+            node = tree;
+          } else {
+            this._publish(tree.c[path[index]], path, index + 1, name, msg, pcb, pcid);
+            this._publish(tree.c["*"], path, index + 1, name, msg, pcb, pcid);			
+            node = tree.c["**"];
+          }
+          if(typeof node != "undefined") {
+            var callbacks = node.s;
+            var max = callbacks.length;
+            for(var i = 0; i < max; i++) {
+              if(callbacks[i].cb) {
+                var sc = callbacks[i].scope;
+                var cb = callbacks[i].cb;
+                var fcb = callbacks[i].fcb;
+                var d = callbacks[i].data;
+                var sid = callbacks[i].sid;
+                var scid = callbacks[i].cid;
+                if(typeof cb == "string"){
+                  // get a function object
+                  cb = sc[cb];
+                }
+                if(typeof fcb == "string"){
+                  // get a function object
+                  fcb = sc[fcb];
+                }
+                if((!fcb) || (fcb.call(sc, name, msg, d))) {
+                  if((!pcb) || (pcb(name, msg, pcid, scid))) {
+                    cb.call(sc, name, msg, d, sid);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+        
+      h._unsubscribe = function(tree, path, index, sid) {
+        if(typeof tree != "undefined") {
+          if(index < path.length) {
+            var childNode = tree.c[path[index]];
+            this._unsubscribe(childNode, path, index + 1, sid);
+            if(childNode.s.length == 0) {
+              for(var x in childNode.c) 
+                return;		
+              delete tree.c[path[index]];	
+            }
+            return;
+          }
+          else {
+            var callbacks = tree.s;
+            var max = callbacks.length;
+            for(var i = 0; i < max; i++) 
+              if(sid == callbacks[i].sid) {
+                if(this._pubDepth > 0) {
+                  callbacks[i].cb = null;	
+                  this._cleanup.push(callbacks[i]);						
+                }
+                else
+                  callbacks.splice(i, 1);
+                return; 	
+              }
+          }
+        }
+      }
+      // The following function is provided for automatic testing purposes.
+      // It is not expected to be deployed in run-time OpenAjax Hub implementations.
+      h.reinit = function()
+      {
+        for (var lib in OpenAjax.hub.libraries) {
+          delete OpenAjax.hub.libraries[lib];
+        }
+        OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "1.0", {});
+
+        delete OpenAjax._subscriptions;
+        OpenAjax._subscriptions = {c:{},s:[]};
+        delete OpenAjax._cleanup;
+        OpenAjax._cleanup = [];
+        OpenAjax._subIndex = 0;
+        OpenAjax._pubDepth = 0;
+      }
+    };
+    // Register the OpenAjax Hub itself as a library.
+    OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "1.0", {});
+  }
+
+}());
diff --git a/resources/static/shared/class.js b/resources/static/shared/class.js
new file mode 100644
index 000000000..2668bf247
--- /dev/null
+++ b/resources/static/shared/class.js
@@ -0,0 +1,56 @@
+
+BrowserID.Class = (function() {
+  function create(constr, config) {
+    var inst = new constr;
+    inst.init(config);
+    return inst;
+  }
+
+  function extend(sup, extension) {
+    // No superclass
+    if(!extension) {
+      extension = sup;
+      sup = null;
+    }
+
+    var subclass = extension.hasOwnProperty("constructor") ? extension.constructor : function() {};
+
+    if(sup) {
+      // there is a superclass, set it up.
+      // Object.create would work well here.
+      var F = function() {};
+      F.prototype = sup.prototype;
+      subclass.prototype = new F;
+      subclass.sc = sup.prototype;
+    }
+    else {
+      // no superclass, create a prototype object.
+      subclass.prototype = {};
+    }
+
+    for(var key in extension) {
+      subclass.prototype[key] = extension[key];
+    }
+    subclass.prototype.constructor = subclass;
+
+    /**
+     * Extend a class to create a subclass.
+     * @method extend
+     * @param {object} extensions - prototype extensions
+     * @returns {function} subclass
+     */
+    subclass.extend = extend.bind(null, subclass);
+    /**
+     * Create an instance of a class
+     * @method create
+     * @param {object} [config] - configuration, passed on to init.
+     */
+    subclass.create = create.bind(null, subclass);
+
+    return subclass;
+  }
+
+  return extend;
+
+}());
+
diff --git a/resources/static/shared/mediator.js b/resources/static/shared/mediator.js
new file mode 100644
index 000000000..33faab020
--- /dev/null
+++ b/resources/static/shared/mediator.js
@@ -0,0 +1,46 @@
+/*globals BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+BrowserID.Mediator = (function() {
+  var hub = OpenAjax.hub;
+
+  return {
+    subscribe: hub.subscribe.bind(hub),
+    unsubscribe: hub.unsubscribe.bind(hub),
+    publish: hub.publish.bind(hub)
+  };
+}());
+
diff --git a/resources/static/test/qunit/controllers/addemail_controller_unit_test.js b/resources/static/test/qunit/controllers/addemail_unit_test.js
similarity index 91%
rename from resources/static/test/qunit/controllers/addemail_controller_unit_test.js
rename to resources/static/test/qunit/controllers/addemail_unit_test.js
index 26fb2bc14..38ea061f3 100644
--- a/resources/static/test/qunit/controllers/addemail_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/addemail_unit_test.js
@@ -44,18 +44,19 @@ steal.then(function() {
       user = bid.User,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator, 
+      modules = bid.Modules,
       testOrigin = "http://browserid.org",
       registrations = [];
 
   function register(message, cb) {
-    registrations.push(hub.subscribe(message, cb));
+    registrations.push(mediator.subscribe(message, cb));
   }
 
   function unregisterAll() {
     var registration;
     while(registration = registrations.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
@@ -83,18 +84,18 @@ steal.then(function() {
     }
   });
 
-  function createController() {
-    return $("body").addemail({}).controller();
+  function createController(options) {
+    controller = modules.AddEmail.create(options);
   }
 
   test("addemail controller renders correctly", function() {
-    controller = createController();
+    createController();
 
     equal($("#addEmail").length, 1, "control rendered correctly");
   });
 
   test("addEmail with valid email", function() {
-    controller = createController();
+    createController();
 
     $("#newEmail").val("unregistered@testuser.com");
     register("email_staged", function(msg, info) {
@@ -106,7 +107,7 @@ steal.then(function() {
   });
 
   test("addEmail with valid email with leading/trailing whitespace", function() {
-    controller = createController();
+    createController();
 
     $("#newEmail").val("   unregistered@testuser.com  ");
     register("email_staged", function(msg, info) {
@@ -118,7 +119,7 @@ steal.then(function() {
   });
 
   test("addEmail with invalid email", function() {
-    controller = createController();
+    createController();
 
     $("#newEmail").val("unregistered");
     var handlerCalled = false;
@@ -136,7 +137,7 @@ steal.then(function() {
   });
 
   test("addEmail with previously registered email - allows for account consolidation", function() {
-    controller = createController();
+    createController();
 
     $("#newEmail").val("registered@testuser.com");
     register("email_staged", function(msg, info) {
@@ -148,7 +149,7 @@ steal.then(function() {
   });
 
   test("cancelAddEmail", function() {
-    controller = createController();
+    createController();
 
     register("cancel_add_email", function(msg, info) {
       ok(true, "cancelling the add email");
diff --git a/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
similarity index 95%
rename from resources/static/test/qunit/controllers/authenticate_controller_unit_test.js
rename to resources/static/test/qunit/controllers/authenticate_unit_test.js
index 9039e019a..0090a10f4 100644
--- a/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -45,17 +45,17 @@ steal.then(function() {
       xhr = bid.Mocks.xhr,
       emailRegistered = false,
       userCreated = true,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator,
       registrations = [];
 
   function register(message, cb) {
-    registrations.push(hub.subscribe(message, cb));
+    registrations.push(mediator.subscribe(message, cb));
   }
 
   function unregisterAll() {
     var registration;
     while(registration = registrations.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
@@ -69,13 +69,17 @@ steal.then(function() {
     userCreated = true;
   }
 
+  function createController(options) {
+    controller = bid.Modules.Authenticate.create(options);
+  }
+
   module("controllers/authenticate_controller", {
     setup: function() {
       reset();
       storage.clear();
       network.setXHR(xhr);
       xhr.useResult("valid");
-      controller = el.authenticate().controller();
+      createController();
     },
 
     teardown: function() {
@@ -93,10 +97,11 @@ steal.then(function() {
     }
   });
 
+
   test("setting email address prefills address field", function() {
       controller.destroy();
       $("#email").val("");
-      controller = el.authenticate({ email: "registered@testuser.com" }).controller();
+      createController({ email: "registered@testuser.com" });
       equal($("#email").val(), "registered@testuser.com", "email prefilled");
   });
 
diff --git a/resources/static/test/qunit/controllers/checkregistration_controller_unit_test.js b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
similarity index 88%
rename from resources/static/test/qunit/controllers/checkregistration_controller_unit_test.js
rename to resources/static/test/qunit/controllers/checkregistration_unit_test.js
index ee15db686..199d63703 100644
--- a/resources/static/test/qunit/controllers/checkregistration_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/controllers/checkregistration_controller", function() {
+steal.then(function() {
   "use strict";
 
   var controller,
@@ -42,27 +42,27 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
       bid = BrowserID,
       xhr = bid.Mocks.xhr,
       network = bid.Network,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator,
       listeners = [];
 
   function subscribe(message, cb) {
-    listeners.push(hub.subscribe(message, cb));
+    listeners.push(mediator.subscribe(message, cb));
   }
 
   function unsubscribeAll() {
     var registration;
     while(registration = listeners.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
-  function initController(verifier, message) {
+  function createController(verifier, message) {
     el = $("body");
-    controller = el.checkregistration({
+    controller = bid.Modules.CheckRegistration.create({
       email: "registered@testuser.com",
       verifier: verifier,
       verificationMessage: message
-    }).controller();
+    });
   }
 
   module("controllers/checkregistration_controller", {
@@ -86,7 +86,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   function testVerifiedUserEvent(event_name, message) {
-    initController("waitForUserValidation", event_name);
+    createController("waitForUserValidation", event_name);
     subscribe(event_name, function() {
       ok(true, message);
       start();
@@ -114,7 +114,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   test("user validation with XHR error", function() {
     xhr.useResult("ajaxError");
 
-    initController("waitForUserValidation", "user_verified");
+    createController("waitForUserValidation", "user_verified");
     subscribe("user_verified", function() {
       ok(false, "on XHR error, should not complete");
     });
@@ -129,7 +129,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("cancel raises cancel_user_verified", function() {
-    initController("waitForUserValidation", "user_verified");
+    createController("waitForUserValidation", "user_verified");
     subscribe("cancel_user_verified", function() {
       ok(true, "on cancel, cancel_user_verified is triggered");
       start();
diff --git a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js b/resources/static/test/qunit/controllers/dialog_unit_test.js
similarity index 92%
rename from resources/static/test/qunit/controllers/dialog_controller_unit_test.js
rename to resources/static/test/qunit/controllers/dialog_unit_test.js
index a0cf4452d..f8c910f91 100644
--- a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/controllers/dialog_controller", function() {
+steal.then(function() {
   "use strict";
 
   var controller,
@@ -50,7 +50,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
     channelError = false;
   }
 
-  function initController(config) {
+  function createController(config) {
     var config = $.extend(config, {
       window: {
         setupChannel: function() {
@@ -59,7 +59,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
       }
     });
 
-    controller = el.dialog(config).controller();
+    controller = BrowserID.Modules.Dialog.create(config);
   }
 
   module("controllers/dialog_controller", {
@@ -75,20 +75,20 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
 
   test("initialization with channel error", function() {
     channelError = true;
-    initController();
+    createController();
 
     ok($("#error .contents").text().length, "contents have been written");
   });
 
   test("doOffline", function() {
-    initController();
+    createController();
     controller.doOffline();
     ok($("#error .contents").text().length, "contents have been written");
     ok($("#error #offline").text().length, "offline error message has been written");
   });
 
   test("doXHRError while online, no network info given", function() {
-    initController();
+    createController();
     controller.doXHRError();
     ok($("#error .contents").text().length, "contents have been written");
     ok($("#error #action").text().length, "action contents have been written");
@@ -96,7 +96,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doXHRError while online, network info given", function() {
-    initController();
+    createController();
     controller.doXHRError({
       network: {
         type: "POST",
@@ -109,7 +109,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doXHRError while offline does not update contents", function() {
-    initController();
+    createController();
     controller.doOffline();
     $("#error #action").remove();
 
@@ -120,7 +120,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
 
   /*
   test("doCheckAuth with registered requiredEmail, authenticated", function() {
-    initController({
+    createController({
       requiredEmail: "registered@testuser.com" 
     });
 
@@ -128,7 +128,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doCheckAuth with registered requiredEmail, not authenticated", function() {
-    initController({
+    createController({
       requiredEmail: "registered@testuser.com" 
     });
 
@@ -136,7 +136,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doCheckAuth with unregistered requiredEmail, not authenticated", function() {
-    initController({
+    createController({
       requiredEmail: "unregistered@testuser.com" 
     });
 
@@ -144,7 +144,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   });
 
   test("doCheckAuth with unregistered requiredEmail, authenticated as other user", function() {
-    initController({
+    createController({
       requiredEmail: "unregistered@testuser.com" 
     });
 
diff --git a/resources/static/test/qunit/controllers/forgotpassword_controller_unit_test.js b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
similarity index 92%
rename from resources/static/test/qunit/controllers/forgotpassword_controller_unit_test.js
rename to resources/static/test/qunit/controllers/forgotpassword_unit_test.js
index d42e9fa93..66228f6ab 100644
--- a/resources/static/test/qunit/controllers/forgotpassword_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/forgotpassword_unit_test.js
@@ -43,17 +43,17 @@ steal.then(function() {
       storage = bid.Storage,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator,
       registrations = [];
 
   function register(message, cb) {
-    registrations.push(hub.subscribe(message, cb));
+    registrations.push(mediator.subscribe(message, cb));
   }
 
   function unregisterAll() {
     var registration;
     while(registration = registrations.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
@@ -64,6 +64,10 @@ steal.then(function() {
     el.find("#error .contents").html("");
   }
 
+  function createController(options) {
+    controller = bid.Modules.ForgotPassword.create(options);
+  }
+
   module("controllers/forgotpassword_controller", {
     setup: function() {
       $("#email").val("");
@@ -71,7 +75,7 @@ steal.then(function() {
       storage.clear();
       network.setXHR(xhr);
       xhr.useResult("valid");
-      controller = el.forgotpassword({ email: "registered@testuser.com" }).controller();
+      createController({ email: "registered@testuser.com" });
     },
 
     teardown: function() {
diff --git a/resources/static/test/qunit/controllers/page_controller_unit_test.js b/resources/static/test/qunit/controllers/page_unit_test.js
similarity index 89%
rename from resources/static/test/qunit/controllers/page_controller_unit_test.js
rename to resources/static/test/qunit/controllers/page_unit_test.js
index 00a21936d..99295d5b3 100644
--- a/resources/static/test/qunit/controllers/page_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/page_unit_test.js
@@ -34,12 +34,15 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
+steal.then(function() {
   "use strict";
 
   var controller, el,
       bodyTemplate = "testBodyTemplate",
-      waitTemplate = "wait";
+      waitTemplate = "wait",
+      bid = BrowserID,
+      mediator = bid.Mediator,
+      modules = bid.Modules;
 
   function reset() {
     el = $("#controller_head");
@@ -49,6 +52,10 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     el.find("#error .contents").html("");
   }
 
+  function createController(options) {
+    controller = bid.Modules.PageModule.create(options);
+  }
+
   module("/controllers/page_controller", {
     setup: function() {
       reset();
@@ -61,7 +68,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("page controller with no template causes no side effects", function() {
-    controller = el.page().controller();
+    createController();
 
     var html = el.find("#formWrap .contents").html();
     equal(html, "", "with no template specified, no text is loaded");
@@ -71,13 +78,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("page controller with body template renders in #formWrap .contents", function() {
-    controller = el.page({
+    createController({
       bodyTemplate: bodyTemplate,
       bodyVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     var html = el.find("#formWrap .contents").html();
     ok(html.length, "with template specified, form text is loaded");
@@ -92,13 +99,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("page controller with wait template renders in #wait .contents", function() {
-    controller = el.page({
+    createController({
       waitTemplate: waitTemplate,
       waitVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     var html = el.find("#formWrap .contents").html();
     equal(html, "", "with wait template specified, form is ignored");
@@ -108,13 +115,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("page controller with error template renders in #error .contents", function() {
-    controller = el.page({
+    createController({
       errorTemplate: waitTemplate,
       errorVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     var html = el.find("#formWrap .contents").html();
     equal(html, "", "with error template specified, form is ignored");
@@ -124,13 +131,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("renderError renders an error message", function() {
-    controller = el.page({
+    createController({
       waitTemplate: waitTemplate,
       waitVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     controller.renderError("wait", {
       title: "error title",
@@ -143,13 +150,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("renderError allows us to open expanded error info", function() {
-    controller = el.page({
+    createController({
       waitTemplate: waitTemplate,
       waitVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     controller.renderError("error", {
       action: {
@@ -174,13 +181,13 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("getErrorDialog gets a function that can be used to render an error message", function() {
-    controller = el.page({
+    createController({
       waitTemplate: waitTemplate,
       waitVars: {
         title: "Test title",
         message: "Test message"
       }
-    }).controller();
+    });
 
     // This is the medium level info.
     var func = controller.getErrorDialog({
@@ -198,7 +205,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("bind DOM Events", function() {
-    controller = el.page().controller();
+    createController();
 
    controller.bind("body", "click", function(event) {
       event.preventDefault();
@@ -212,7 +219,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   });
 
   test("unbindAll removes all listeners", function() {
-    controller = el.page().controller();
+    createController();
     var listenerCalled = false;
 
     controller.bind("body", "click", function(event) {
@@ -232,5 +239,19 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     }, 100);
   });
 
+  test("publish", function() {
+    createController();
+
+    mediator.subscribe("message", function(msg, data) {
+      equal(msg, "message", "message is correct");
+      equal(data.field, 1, "data passed correctly");
+      start();
+    });
+
+    controller.publish("message", {
+      field: 1
+    });
+  });
+
 });
 
diff --git a/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js b/resources/static/test/qunit/controllers/pickemail_unit_test.js
similarity index 97%
rename from resources/static/test/qunit/controllers/pickemail_controller_unit_test.js
rename to resources/static/test/qunit/controllers/pickemail_unit_test.js
index 42822c93c..f9e2611b2 100644
--- a/resources/static/test/qunit/controllers/pickemail_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/pickemail_unit_test.js
@@ -44,18 +44,18 @@ steal.then(function() {
       user = bid.User,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator,
       testOrigin = "http://browserid.org",
       registrations = [];
 
   function register(message, cb) {
-    registrations.push(hub.subscribe(message, cb));
+    registrations.push(mediator.subscribe(message, cb));
   }
 
   function unregisterAll() {
     var registration;
     while(registration = registrations.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
@@ -93,9 +93,9 @@ steal.then(function() {
 
 
   function createController(allowPersistent) {
-    controller = el.pickemail({
+    controller = bid.Modules.PickEmail.create({
       allow_persistent: allowPersistent || false
-    }).controller();
+    });
   }
 
   test("pickemail controller with email associated with site", function() {
diff --git a/resources/static/test/qunit/controllers/required_email_controller_unit_test.js b/resources/static/test/qunit/controllers/required_email_unit_test.js
similarity index 91%
rename from resources/static/test/qunit/controllers/required_email_controller_unit_test.js
rename to resources/static/test/qunit/controllers/required_email_unit_test.js
index 8eef389d6..6ff0ee75c 100644
--- a/resources/static/test/qunit/controllers/required_email_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/required_email_unit_test.js
@@ -44,18 +44,18 @@ steal.then(function() {
       user = bid.User,
       network = bid.Network,
       storage = bid.Storage,
-      hub = OpenAjax.hub,
+      mediator = bid.Mediator,
       listeners = [];
 
   // XXX TODO Share this code with the other tests.
   function subscribe(message, cb) {
-    listeners.push(hub.subscribe(message, cb));
+    listeners.push(mediator.subscribe(message, cb));
   }
 
   function unsubscribeAll() {
     var registration;
     while(registration = listeners.pop()) {
-      hub.unsubscribe(registration);
+      mediator.unsubscribe(registration);
     }
   }
 
@@ -86,6 +86,10 @@ steal.then(function() {
     }
   });
 
+  function createController(options) {
+    controller = bid.Modules.RequiredEmail.create(options);
+  }
+
   function testSignIn(email, cb) {
     setTimeout(function() {
       var el = $("#required_email");
@@ -121,20 +125,20 @@ steal.then(function() {
 
   test("user who is not authenticated, email is registered", function() {
     var email = "registered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: false
-    }).controller();
+    });
 
     testSignIn(email, testPasswordSection);
   });
 
   test("user who is not authenticated, email not registered", function() {
     var email = "unregistered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: false
-    }).controller();
+    });
 
     testVerify(email);
   });
@@ -142,10 +146,10 @@ steal.then(function() {
   test("user who is not authenticated, XHR error", function() {
     xhr.useResult("ajaxError");
     var email = "registered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: false
-    }).controller();
+    });
 
     stop();
 
@@ -162,10 +166,10 @@ steal.then(function() {
 
     var email = "registered@testuser.com";
     user.syncEmailKeypair(email, function() {
-      controller = el.requiredemail({
+      createController({
         email: email, 
         authenticated: true
-      }).controller();
+      });
     });
 
     testSignIn(email, testNoPasswordSection);
@@ -177,10 +181,10 @@ steal.then(function() {
     });
 
     var email = "registered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: true
-    }).controller();
+    });
 
     // This means the current user is going to take the address from the other 
     // account.
@@ -193,10 +197,10 @@ steal.then(function() {
     });
 
     var email = "unregistered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: true
-    }).controller();
+    });
 
     testVerify(email);
   });
@@ -209,10 +213,10 @@ steal.then(function() {
 
     var email = "registered@testuser.com";
     user.syncEmailKeypair(email, function() {
-      controller = el.requiredemail({
+      createController({
         email: email, 
         authenticated: true
-      }).controller();
+      });
 
       subscribe("assertion_generated", function(item, info) {
         ok(info.assertion, "we have an assertion");
@@ -231,10 +235,10 @@ steal.then(function() {
     });
 
     var email = "registered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: false
-    }).controller();
+    });
 
     subscribe("assertion_generated", function(item, info) {
       ok(info.assertion, "we have an assertion");
@@ -254,10 +258,10 @@ steal.then(function() {
     });
 
     var email = "registered@testuser.com";
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: false
-    }).controller();
+    });
 
     var assertion;
 
@@ -288,10 +292,10 @@ steal.then(function() {
       authenticated: authenticated 
     });
 
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: authenticated
-    }).controller();
+    });
 
 
     subscribe(message, function(item, info) {
@@ -324,10 +328,10 @@ steal.then(function() {
       authenticated: authenticated 
     });
 
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: authenticated
-    }).controller();
+    });
 
 
     subscribe(message, function(item, info) {
@@ -348,10 +352,10 @@ steal.then(function() {
       authenticated: authenticated 
     });
 
-    controller = el.requiredemail({
+    createController({
       email: email, 
       authenticated: authenticated
-    }).controller();
+    });
 
 
     subscribe(message, function(item, info) {
diff --git a/resources/static/test/qunit/shared/class_unit_test.js b/resources/static/test/qunit/shared/class_unit_test.js
new file mode 100644
index 000000000..46e39af26
--- /dev/null
+++ b/resources/static/test/qunit/shared/class_unit_test.js
@@ -0,0 +1,134 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.then(function() {
+  module("shared/class", {
+  });
+
+  test("create a class with no superclass", function() {
+    var Class = BrowserID.Class({
+      constructor: function() {
+        this.constRun = true;
+      },
+
+      init: function(config) {
+        this.val = true;
+      },
+
+      getVal: function() {
+        return this.val;
+      }
+    });
+
+    equal(typeof Class, "function", "Class created");
+    equal(typeof Class.sc, "undefined", "no superclass");
+
+    var inst = Class.create();
+    ok(inst instanceof Class, "instance created");
+    equal(inst.constRun, true, "constructor was run");
+    equal(inst.getVal(), true, "init was run, getVal correctly added");
+  });
+
+  test("create a class with a superclass", function() {
+    var Sup = BrowserID.Class({
+      init: function() { },
+
+      val: true,
+      getVal: function() {
+        return this.val;
+      },
+
+      anotherVal: 3,
+      getAnotherVal: function() {
+        return this.anotherVal;
+      }
+    });
+
+    var Sub = BrowserID.Class(Sup, {
+      val2: false,
+      getVal2: function() {
+        return this.val2;
+      },
+
+      getAnotherVal: function() {
+        return Sub.sc.getAnotherVal.call(this) + 1;
+      }
+    });
+
+    strictEqual(Sub.sc, Sup.prototype, "Sub classes superclass points to Sup.prototype");
+    var inst = Sub.create();
+
+    equal(inst.getVal(), true, "superclass function added");
+    equal(inst.getVal2(), false, "sublcass function added");
+    equal(inst.getAnotherVal(), 4, "overridden function works properly");
+  });
+
+  test("Class.extend", function() {
+    var Sup = BrowserID.Class({
+      init: function() { },
+
+      val: true,
+      getVal: function() {
+        return this.val;
+      },
+
+      anotherVal: 3,
+      getAnotherVal: function() {
+        return this.anotherVal;
+      }
+    });
+
+    var Sub = Sup.extend({
+      val2: false,
+      getVal2: function() {
+        return this.val2;
+      },
+
+      getAnotherVal: function() {
+        return Sub.sc.getAnotherVal.call(this) + 1;
+      }
+    });
+
+    strictEqual(Sub.sc, Sup.prototype, "Sub classes superclass points to Sup.prototype");
+    var inst = Sub.create();
+
+    equal(inst.getVal(), true, "superclass function added");
+    equal(inst.getVal2(), false, "sublcass function added");
+    equal(inst.getAnotherVal(), 4, "overridden function works properly");
+
+  });
+
+});
-- 
GitLab