Bug 1065608 Drop the remaining backbone views for Loop (switch to react). r=nperriault
authorMark Banner <standard8@mozilla.com>
Thu, 11 Sep 2014 08:28:02 +0100
changeset 204889 e00245b62a02e13bc3bd1fec11aaea373314da9e
parent 204888 2621e03cb4e4d1a7b1619abef77f62d541c59d3a
child 204890 fb4b70803019549a277597bf3477c45f68f25035
push id27469
push userryanvm@gmail.com
push dateThu, 11 Sep 2014 22:11:17 +0000
treeherdermozilla-central@c3ba2da30e86 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1065608
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1065608 Drop the remaining backbone views for Loop (switch to react). r=nperriault
browser/components/loop/content/shared/js/router.js
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/standalone/content/js/webapp.js
browser/components/loop/standalone/content/js/webapp.jsx
browser/components/loop/standalone/content/l10n/loop.en-US.properties
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/shared/router_test.js
browser/components/loop/test/shared/views_test.js
browser/components/loop/test/standalone/webapp_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/shared/js/router.js
+++ b/browser/components/loop/content/shared/js/router.js
@@ -12,22 +12,16 @@ loop.shared.router = (function() {
   /**
    * Base Router. Allows defining a main active view and ease toggling it when
    * the active route changes.
    *
    * @link http://mikeygee.com/blog/backbone.html
    */
   var BaseRouter = Backbone.Router.extend({
     /**
-     * Active view.
-     * @type {Object}
-     */
-    _activeView: undefined,
-
-    /**
      * Notifications collection.
      * @type {loop.shared.models.NotificationCollection}
      */
     _notifications: undefined,
 
     /**
      * Constructor.
      *
@@ -42,61 +36,31 @@ loop.shared.router = (function() {
         throw new Error("missing required notifications");
       }
       this._notifications = options.notifications;
 
       Backbone.Router.apply(this, arguments);
     },
 
     /**
-     * Loads and render current active view.
-     *
-     * @param {loop.shared.views.BaseView} view View.
-     */
-    loadView: function(view) {
-      this.clearActiveView();
-      this._activeView = {type: "backbone", view: view.render().show()};
-      this.updateView(this._activeView.view.$el);
-    },
-
-    /**
      * Renders a React component as current active view.
      *
      * @param {React} reactComponent React component.
      */
     loadReactComponent: function(reactComponent) {
       this.clearActiveView();
-      this._activeView = {
-        type: "react",
-        view: React.renderComponent(reactComponent,
-                                    document.querySelector("#main"))
-      };
+      React.renderComponent(reactComponent,
+                            document.querySelector("#main"));
     },
 
     /**
      * Clears current active view.
      */
     clearActiveView: function() {
-      if (!this._activeView) {
-        return;
-      }
-      if (this._activeView.type === "react") {
-        React.unmountComponentAtNode(document.querySelector("#main"));
-      } else {
-        this._activeView.view.remove();
-      }
-    },
-
-    /**
-     * Updates main div element with provided contents.
-     *
-     * @param  {jQuery} $el Element.
-     */
-    updateView: function($el) {
-      $("#main").html($el);
+      React.unmountComponentAtNode(document.querySelector("#main"));
     }
   });
 
   /**
    * Base conversation router, implementing common behaviors when handling
    * a conversation.
    */
   var BaseConversationRouter = BaseRouter.extend({
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -10,93 +10,16 @@ var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = (function(_, OT, l10n) {
   "use strict";
 
   var sharedModels = loop.shared.models;
   var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
 
   /**
-   * L10n view. Translates resulting view DOM fragment once rendered.
-   */
-  var L10nView = (function() {
-    var L10nViewImpl   = Backbone.View.extend(), // Original View constructor
-        originalExtend = L10nViewImpl.extend;    // Original static extend fn
-
-    /**
-     * Patches View extend() method so we can hook and patch any declared render
-     * method.
-     *
-     * @return {Backbone.View} Extended view with patched render() method.
-     */
-    L10nViewImpl.extend = function() {
-      var ExtendedView   = originalExtend.apply(this, arguments),
-          originalRender = ExtendedView.prototype.render;
-
-      /**
-       * Wraps original render() method to translate contents once they're
-       * rendered.
-       *
-       * @return {Backbone.View} Extended view instance.
-       */
-      ExtendedView.prototype.render = function() {
-        if (originalRender) {
-          originalRender.apply(this, arguments);
-          l10n.translate(this.el);
-        }
-        return this;
-      };
-
-      return ExtendedView;
-    };
-
-    return L10nViewImpl;
-  })();
-
-  /**
-   * Base view.
-   */
-  var BaseView = L10nView.extend({
-    /**
-     * Hides view element.
-     *
-     * @return {BaseView}
-     */
-    hide: function() {
-      this.$el.hide();
-      return this;
-    },
-
-    /**
-     * Shows view element.
-     *
-     * @return {BaseView}
-     */
-    show: function() {
-      this.$el.show();
-      return this;
-    },
-
-    /**
-     * Base render implementation: renders an attached template if available.
-     *
-     * Note: You need to override this if you want to do fancier stuff, eg.
-     *       rendering the template using model data.
-     *
-     * @return {BaseView}
-     */
-    render: function() {
-      if (this.template) {
-        this.$el.html(this.template());
-      }
-      return this;
-    }
-  });
-
-  /**
    * Media control button.
    *
    * Required props:
    * - {String}   scope   Media scope, can be "local" or "remote".
    * - {String}   type    Media type, can be "audio" or "video".
    * - {Function} action  Function to be executed on click.
    * - {Enabled}  enabled Stream activation status (default: true).
    */
@@ -699,48 +622,16 @@ loop.shared.views = (function(_, OT, l10
             return NotificationView({key: key, notification: notification});
           })
         
         )
       );
     }
   });
 
-  /**
-   * Unsupported Browsers view.
-   */
-  var UnsupportedBrowserView = BaseView.extend({
-    template: _.template([
-      '<div>',
-      '  <h2 data-l10n-id="incompatible_browser"></h2>',
-      '  <p data-l10n-id="powered_by_webrtc"></p>',
-      '  <p data-l10n-id="use_latest_firefox" ',
-      '    data-l10n-args=\'{"ff_url": "https://www.mozilla.org/firefox/"}\'>',
-      '  </p>',
-      '</div>'
-    ].join(""))
-  });
-
-  /**
-   * Unsupported Browsers view.
-   */
-  var UnsupportedDeviceView = BaseView.extend({
-    template: _.template([
-      '<div>',
-      '  <h2 data-l10n-id="incompatible_device"></h2>',
-      '  <p data-l10n-id="sorry_device_unsupported"></p>',
-      '  <p data-l10n-id="use_firefox_windows_mac_linux"></p>',
-      '</div>'
-    ].join(""))
-  });
-
   return {
-    L10nView: L10nView,
-    BaseView: BaseView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     FeedbackView: FeedbackView,
     MediaControlButton: MediaControlButton,
-    NotificationListView: NotificationListView,
-    UnsupportedBrowserView: UnsupportedBrowserView,
-    UnsupportedDeviceView: UnsupportedDeviceView
+    NotificationListView: NotificationListView
   };
 })(_, window.OT, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -10,93 +10,16 @@ var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = (function(_, OT, l10n) {
   "use strict";
 
   var sharedModels = loop.shared.models;
   var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
 
   /**
-   * L10n view. Translates resulting view DOM fragment once rendered.
-   */
-  var L10nView = (function() {
-    var L10nViewImpl   = Backbone.View.extend(), // Original View constructor
-        originalExtend = L10nViewImpl.extend;    // Original static extend fn
-
-    /**
-     * Patches View extend() method so we can hook and patch any declared render
-     * method.
-     *
-     * @return {Backbone.View} Extended view with patched render() method.
-     */
-    L10nViewImpl.extend = function() {
-      var ExtendedView   = originalExtend.apply(this, arguments),
-          originalRender = ExtendedView.prototype.render;
-
-      /**
-       * Wraps original render() method to translate contents once they're
-       * rendered.
-       *
-       * @return {Backbone.View} Extended view instance.
-       */
-      ExtendedView.prototype.render = function() {
-        if (originalRender) {
-          originalRender.apply(this, arguments);
-          l10n.translate(this.el);
-        }
-        return this;
-      };
-
-      return ExtendedView;
-    };
-
-    return L10nViewImpl;
-  })();
-
-  /**
-   * Base view.
-   */
-  var BaseView = L10nView.extend({
-    /**
-     * Hides view element.
-     *
-     * @return {BaseView}
-     */
-    hide: function() {
-      this.$el.hide();
-      return this;
-    },
-
-    /**
-     * Shows view element.
-     *
-     * @return {BaseView}
-     */
-    show: function() {
-      this.$el.show();
-      return this;
-    },
-
-    /**
-     * Base render implementation: renders an attached template if available.
-     *
-     * Note: You need to override this if you want to do fancier stuff, eg.
-     *       rendering the template using model data.
-     *
-     * @return {BaseView}
-     */
-    render: function() {
-      if (this.template) {
-        this.$el.html(this.template());
-      }
-      return this;
-    }
-  });
-
-  /**
    * Media control button.
    *
    * Required props:
    * - {String}   scope   Media scope, can be "local" or "remote".
    * - {String}   type    Media type, can be "audio" or "video".
    * - {Function} action  Function to be executed on click.
    * - {Enabled}  enabled Stream activation status (default: true).
    */
@@ -699,48 +622,16 @@ loop.shared.views = (function(_, OT, l10
             return <NotificationView key={key} notification={notification}/>;
           })
         }
         </div>
       );
     }
   });
 
-  /**
-   * Unsupported Browsers view.
-   */
-  var UnsupportedBrowserView = BaseView.extend({
-    template: _.template([
-      '<div>',
-      '  <h2 data-l10n-id="incompatible_browser"></h2>',
-      '  <p data-l10n-id="powered_by_webrtc"></p>',
-      '  <p data-l10n-id="use_latest_firefox" ',
-      '    data-l10n-args=\'{"ff_url": "https://www.mozilla.org/firefox/"}\'>',
-      '  </p>',
-      '</div>'
-    ].join(""))
-  });
-
-  /**
-   * Unsupported Browsers view.
-   */
-  var UnsupportedDeviceView = BaseView.extend({
-    template: _.template([
-      '<div>',
-      '  <h2 data-l10n-id="incompatible_device"></h2>',
-      '  <p data-l10n-id="sorry_device_unsupported"></p>',
-      '  <p data-l10n-id="use_firefox_windows_mac_linux"></p>',
-      '</div>'
-    ].join(""))
-  });
-
   return {
-    L10nView: L10nView,
-    BaseView: BaseView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     FeedbackView: FeedbackView,
     MediaControlButton: MediaControlButton,
-    NotificationListView: NotificationListView,
-    UnsupportedBrowserView: UnsupportedBrowserView,
-    UnsupportedDeviceView: UnsupportedDeviceView
+    NotificationListView: NotificationListView
   };
 })(_, window.OT, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -22,18 +22,57 @@ loop.webapp = (function($, _, OT, mozL10
    * App router.
    * @type {loop.webapp.WebappRouter}
    */
   var router;
 
   /**
    * Homepage view.
    */
-  var HomeView = sharedViews.BaseView.extend({
-    template: _.template('<p data-l10n-id="welcome"></p>')
+  var HomeView = React.createClass({displayName: 'HomeView',
+    render: function() {
+      return (
+        React.DOM.p(null, mozL10n.get("welcome"))
+      )
+    }
+  });
+
+  /**
+   * Unsupported Browsers view.
+   */
+  var UnsupportedBrowserView = React.createClass({displayName: 'UnsupportedBrowserView',
+    render: function() {
+      var useLatestFF = mozL10n.get("use_latest_firefox", {
+        "firefoxBrandNameLink": React.renderComponentToStaticMarkup(
+          React.DOM.a({target: "_blank", href: "https://www.mozilla.org/firefox/"}, "Firefox")
+        )
+      });
+      return (
+        React.DOM.div(null, 
+          React.DOM.h2(null, mozL10n.get("incompatible_browser")), 
+          React.DOM.p(null, mozL10n.get("powered_by_webrtc")), 
+          React.DOM.p({dangerouslySetInnerHTML: {__html: useLatestFF}})
+        )
+      );
+    }
+  });
+
+  /**
+   * Unsupported Device view.
+   */
+  var UnsupportedDeviceView = React.createClass({displayName: 'UnsupportedDeviceView',
+    render: function() {
+      return (
+        React.DOM.div(null, 
+          React.DOM.h2(null, mozL10n.get("incompatible_device")), 
+          React.DOM.p(null, mozL10n.get("sorry_device_unsupported")), 
+          React.DOM.p(null, mozL10n.get("use_firefox_windows_mac_linux"))
+        )
+      );
+    }
   });
 
   /**
    * Firefox promotion interstitial. Will display only to non-Firefox users.
    */
   var PromoteFirefoxView = React.createClass({displayName: 'PromoteFirefoxView',
     propTypes: {
       helper: React.PropTypes.object.isRequired
@@ -319,17 +358,17 @@ loop.webapp = (function($, _, OT, mozL10
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
         throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
-      this.loadView(new HomeView());
+      this.loadReactComponent(HomeView(null));
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
@@ -465,25 +504,25 @@ loop.webapp = (function($, _, OT, mozL10
     _onTimeout: function() {
       this._notifications.errorL10n("call_timeout_notification_text");
     },
 
     /**
      * Default entry point.
      */
     home: function() {
-      this.loadView(new HomeView());
+      this.loadReactComponent(HomeView(null));
     },
 
     unsupportedDevice: function() {
-      this.loadView(new sharedViews.UnsupportedDeviceView());
+      this.loadReactComponent(UnsupportedDeviceView(null));
     },
 
     unsupportedBrowser: function() {
-      this.loadView(new sharedViews.UnsupportedBrowserView());
+      this.loadReactComponent(UnsupportedBrowserView(null));
     },
 
     expired: function() {
       this.loadReactComponent(CallUrlExpiredView({helper: this.helper}));
     },
 
     /**
      * Loads conversation launcher view, setting the received conversation token
@@ -574,14 +613,16 @@ loop.webapp = (function($, _, OT, mozL10
     document.documentElement.dir = mozL10n.language.direction;
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
     StartConversationView: StartConversationView,
     HomeView: HomeView,
+    UnsupportedBrowserView: UnsupportedBrowserView,
+    UnsupportedDeviceView: UnsupportedDeviceView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -22,18 +22,57 @@ loop.webapp = (function($, _, OT, mozL10
    * App router.
    * @type {loop.webapp.WebappRouter}
    */
   var router;
 
   /**
    * Homepage view.
    */
-  var HomeView = sharedViews.BaseView.extend({
-    template: _.template('<p data-l10n-id="welcome"></p>')
+  var HomeView = React.createClass({
+    render: function() {
+      return (
+        <p>{mozL10n.get("welcome")}</p>
+      )
+    }
+  });
+
+  /**
+   * Unsupported Browsers view.
+   */
+  var UnsupportedBrowserView = React.createClass({
+    render: function() {
+      var useLatestFF = mozL10n.get("use_latest_firefox", {
+        "firefoxBrandNameLink": React.renderComponentToStaticMarkup(
+          <a target="_blank" href="https://www.mozilla.org/firefox/">Firefox</a>
+        )
+      });
+      return (
+        <div>
+          <h2>{mozL10n.get("incompatible_browser")}</h2>
+          <p>{mozL10n.get("powered_by_webrtc")}</p>
+          <p dangerouslySetInnerHTML={{__html: useLatestFF}}></p>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Unsupported Device view.
+   */
+  var UnsupportedDeviceView = React.createClass({
+    render: function() {
+      return (
+        <div>
+          <h2>{mozL10n.get("incompatible_device")}</h2>
+          <p>{mozL10n.get("sorry_device_unsupported")}</p>
+          <p>{mozL10n.get("use_firefox_windows_mac_linux")}</p>
+        </div>
+      );
+    }
   });
 
   /**
    * Firefox promotion interstitial. Will display only to non-Firefox users.
    */
   var PromoteFirefoxView = React.createClass({
     propTypes: {
       helper: React.PropTypes.object.isRequired
@@ -319,17 +358,17 @@ loop.webapp = (function($, _, OT, mozL10
 
     initialize: function(options) {
       this.helper = options.helper;
       if (!this.helper) {
         throw new Error("WebappRouter requires a helper object");
       }
 
       // Load default view
-      this.loadView(new HomeView());
+      this.loadReactComponent(<HomeView />);
 
       this.listenTo(this._conversation, "timeout", this._onTimeout);
     },
 
     _onSessionExpired: function() {
       this.navigate("/call/expired", {trigger: true});
     },
 
@@ -465,25 +504,25 @@ loop.webapp = (function($, _, OT, mozL10
     _onTimeout: function() {
       this._notifications.errorL10n("call_timeout_notification_text");
     },
 
     /**
      * Default entry point.
      */
     home: function() {
-      this.loadView(new HomeView());
+      this.loadReactComponent(<HomeView />);
     },
 
     unsupportedDevice: function() {
-      this.loadView(new sharedViews.UnsupportedDeviceView());
+      this.loadReactComponent(<UnsupportedDeviceView />);
     },
 
     unsupportedBrowser: function() {
-      this.loadView(new sharedViews.UnsupportedBrowserView());
+      this.loadReactComponent(<UnsupportedBrowserView />);
     },
 
     expired: function() {
       this.loadReactComponent(CallUrlExpiredView({helper: this.helper}));
     },
 
     /**
      * Loads conversation launcher view, setting the received conversation token
@@ -574,14 +613,16 @@ loop.webapp = (function($, _, OT, mozL10
     document.documentElement.dir = mozL10n.language.direction;
   }
 
   return {
     baseServerUrl: baseServerUrl,
     CallUrlExpiredView: CallUrlExpiredView,
     StartConversationView: StartConversationView,
     HomeView: HomeView,
+    UnsupportedBrowserView: UnsupportedBrowserView,
+    UnsupportedDeviceView: UnsupportedDeviceView,
     init: init,
     PromoteFirefoxView: PromoteFirefoxView,
     WebappHelper: WebappHelper,
     WebappRouter: WebappRouter
   };
 })(jQuery, _, window.OT, navigator.mozL10n);
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -18,17 +18,17 @@ mute_local_audio_button_title=Mute your 
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title=Mute your video
 unmute_local_video_button_title=Unmute your video
 outgoing_call_title=Start conversation?
 call_with_contact_title=Conversation with {{incomingCallIdentity}}
 welcome=Welcome to the {{clientShortname}} web client.
 incompatible_browser=Incompatible Browser
 powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
-use_latest_firefox.innerHTML=Please try this link in a WebRTC-enabled browser, such as <a href="{{ff_url}}">{{brandShortname}}</a>.
+use_latest_firefox=Please try this link in a WebRTC-enabled browser, such as {{firefoxBrandNameLink}}.
 incompatible_device=Incompatible device
 sorry_device_unsupported=Sorry, {{clientShortname}} does not currently support your device.
 use_firefox_windows_mac_linux=Please open this page using the latest {{brandShortname}} on Windows, Android, Mac or Linux.
 connection_error_see_console_notification=Call failed; see console for details.
 call_url_unavailable_notification_heading=Oops!
 call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
 promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
 get_firefox_button=Get {{brandShortname}}
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -121,17 +121,16 @@ describe("loop.conversation", function()
       var router;
 
       beforeEach(function() {
         router = new ConversationRouter({
           client: client,
           conversation: conversation,
           notifications: notifications
         });
-        sandbox.stub(router, "loadView");
         sandbox.stub(conversation, "incoming");
       });
 
       describe("#incoming", function() {
 
         // XXX refactor to Just Work with "sandbox.stubComponent" or else
         // just pass in the sandbox and put somewhere generally usable
 
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -73,17 +73,16 @@ describe("loop.panel", function() {
       var router;
 
       beforeEach(function() {
         router = createTestRouter({
           hidden: true,
           addEventListener: sandbox.spy()
         });
 
-        sandbox.stub(router, "loadView");
         sandbox.stub(router, "loadReactComponent");
       });
 
       describe("#home", function() {
         it("should reset the PanelView", function() {
           sandbox.stub(router, "reset");
 
           router.home();
--- a/browser/components/loop/test/shared/router_test.js
+++ b/browser/components/loop/test/shared/router_test.js
@@ -43,54 +43,16 @@ describe("loop.shared.router", function(
 
         it("should require a notifications collection", function() {
           expect(function() {
             new ExtendedRouter();
           }).to.Throw(Error, /missing required notifications/);
         });
       });
     });
-
-    describe("constructed", function() {
-      var router, view, TestRouter;
-
-      beforeEach(function() {
-        TestRouter = loop.shared.router.BaseRouter.extend({});
-        var TestView = loop.shared.views.BaseView.extend({
-          template: _.template("<p>plop</p>")
-        });
-        view = new TestView();
-        router = new TestRouter({notifications: notifications});
-      });
-
-      describe("#loadView", function() {
-        it("should set the active view", function() {
-          router.loadView(view);
-
-          expect(router._activeView).eql({
-            type: "backbone",
-            view: view
-          });
-        });
-
-        it("should load and render the passed view", function() {
-          router.loadView(view);
-
-          expect($("#main p").text()).eql("plop");
-        });
-      });
-
-      describe("#updateView", function() {
-        it("should update the main element with provided contents", function() {
-          router.updateView($("<p>plip</p>"));
-
-          expect($("#main p").text()).eql("plip");
-        });
-      });
-    });
   });
 
   describe("BaseConversationRouter", function() {
     var conversation, TestRouter;
 
     beforeEach(function() {
       TestRouter = loop.shared.router.BaseConversationRouter.extend({
         endCall: sandbox.spy()
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -22,32 +22,16 @@ describe("loop.shared.views", function()
     sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
   });
 
   afterEach(function() {
     $("#fixtures").empty();
     sandbox.restore();
   });
 
-  describe("L10nView", function() {
-    beforeEach(function() {
-      sandbox.stub(l10n, "translate");
-    });
-
-    it("should translate generated contents on render()", function() {
-      var TestView = loop.shared.views.L10nView.extend();
-
-      var view = new TestView();
-      view.render();
-
-      sinon.assert.calledOnce(l10n.translate);
-      sinon.assert.calledWithExactly(l10n.translate, view.el);
-    });
-  });
-
   describe("MediaControlButton", function() {
     it("should render an enabled local audio button", function() {
       var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({
         scope: "local",
         type: "audio",
         action: function(){},
         enabled: true
       }));
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -30,16 +30,17 @@ describe("loop.webapp", function() {
   });
 
   describe("#init", function() {
     var WebappRouter;
 
     beforeEach(function() {
       WebappRouter = loop.webapp.WebappRouter;
       sandbox.stub(WebappRouter.prototype, "navigate");
+      sandbox.stub(WebappRouter.prototype, "loadReactComponent");
     });
 
     afterEach(function() {
       Backbone.history.stop();
     });
 
     it("should navigate to the unsupportedDevice route if the sdk detects " +
        "the device is running iOS", function() {
@@ -79,17 +80,16 @@ describe("loop.webapp", function() {
       });
       sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent");
       router = new loop.webapp.WebappRouter({
         helper: {},
         client: client,
         conversation: conversation,
         notifications: notifications
       });
-      sandbox.stub(router, "loadView");
       sandbox.stub(router, "navigate");
     });
 
     describe("#startCall", function() {
       beforeEach(function() {
         sandbox.stub(router, "_setupWebSocketAndCallView");
       });
 
@@ -268,24 +268,33 @@ describe("loop.webapp", function() {
         router.endCall();
 
         sinon.assert.calledOnce(router.navigate);
         sinon.assert.calledWithMatch(router.navigate, "call/fake");
       });
     });
 
     describe("Routes", function() {
+      beforeEach(function() {
+        // In the router's constructor, it loads the home view, we don't
+        // need to test it here, so reset the stub.
+        router.loadReactComponent.reset();
+      });
+
       describe("#home", function() {
         it("should load the HomeView", function() {
           router.home();
 
-          sinon.assert.calledOnce(router.loadView);
-          sinon.assert.calledWith(router.loadView,
-            sinon.match.instanceOf(loop.webapp.HomeView));
-        });
+          sinon.assert.calledOnce(router.loadReactComponent);
+          sinon.assert.calledWith(router.loadReactComponent,
+            sinon.match(function(value) {
+              return React.addons.TestUtils.isDescriptorOfType(
+                value, loop.webapp.HomeView);
+            }));
+       });
       });
 
       describe("#expired", function() {
         it("should load the CallUrlExpiredView view", function() {
           router.expired();
 
           sinon.assert.calledOnce(router.loadReactComponent);
           sinon.assert.calledWith(router.loadReactComponent,
@@ -347,29 +356,35 @@ describe("loop.webapp", function() {
             sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
           });
       });
 
       describe("#unsupportedDevice", function() {
         it("should load the UnsupportedDeviceView", function() {
           router.unsupportedDevice();
 
-          sinon.assert.calledOnce(router.loadView);
-          sinon.assert.calledWith(router.loadView,
-            sinon.match.instanceOf(sharedViews.UnsupportedDeviceView));
+          sinon.assert.calledOnce(router.loadReactComponent);
+          sinon.assert.calledWith(router.loadReactComponent,
+            sinon.match(function(value) {
+              return React.addons.TestUtils.isDescriptorOfType(
+                value, loop.webapp.UnsupportedDeviceView);
+            }));
         });
       });
 
       describe("#unsupportedBrowser", function() {
         it("should load the UnsupportedBrowserView", function() {
           router.unsupportedBrowser();
 
-          sinon.assert.calledOnce(router.loadView);
-          sinon.assert.calledWith(router.loadView,
-            sinon.match.instanceOf(sharedViews.UnsupportedBrowserView));
+          sinon.assert.calledOnce(router.loadReactComponent);
+          sinon.assert.calledWith(router.loadReactComponent,
+            sinon.match(function(value) {
+              return React.addons.TestUtils.isDescriptorOfType(
+                value, loop.webapp.UnsupportedBrowserView);
+            }));
         });
       });
     });
 
     describe("Events", function() {
       var fakeSessionData;
 
       beforeEach(function() {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -12,16 +12,19 @@
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
   var IncomingCallView = loop.conversation.IncomingCallView;
 
   // 2. Standalone webapp
+  var HomeView = loop.webapp.HomeView;
+  var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
+  var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView    = loop.webapp.CallUrlExpiredView;
   var StartConversationView = loop.webapp.StartConversationView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var ConversationView = loop.shared.views.ConversationView;
   var FeedbackView = loop.shared.views.FeedbackView;
 
@@ -309,16 +312,41 @@
               React.DOM.br(null), 
               React.DOM.div({className: "alert alert-error"}, 
                 React.DOM.button({className: "close"}), 
                 React.DOM.p({className: "message"}, 
                   "The person you were calling has ended the conversation."
                 )
               )
             )
+          ), 
+
+          Section({name: "HomeView"}, 
+            Example({summary: "Standalone Home View"}, 
+              React.DOM.div({className: "standalone"}, 
+                HomeView(null)
+              )
+            )
+          ), 
+
+
+          Section({name: "UnsupportedBrowserView"}, 
+            Example({summary: "Standalone Unsupported Browser"}, 
+              React.DOM.div({className: "standalone"}, 
+                UnsupportedBrowserView(null)
+              )
+            )
+          ), 
+
+          Section({name: "UnsupportedDeviceView"}, 
+            Example({summary: "Standalone Unsupported Device"}, 
+              React.DOM.div({className: "standalone"}, 
+                UnsupportedDeviceView(null)
+              )
+            )
           )
 
         )
       );
     }
   });
 
   /**
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -12,16 +12,19 @@
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
   var IncomingCallView = loop.conversation.IncomingCallView;
 
   // 2. Standalone webapp
+  var HomeView = loop.webapp.HomeView;
+  var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
+  var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView    = loop.webapp.CallUrlExpiredView;
   var StartConversationView = loop.webapp.StartConversationView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var ConversationView = loop.shared.views.ConversationView;
   var FeedbackView = loop.shared.views.FeedbackView;
 
@@ -311,16 +314,41 @@
                 <button className="close"></button>
                 <p className="message">
                   The person you were calling has ended the conversation.
                 </p>
               </div>
             </Example>
           </Section>
 
+          <Section name="HomeView">
+            <Example summary="Standalone Home View">
+              <div className="standalone">
+                <HomeView />
+              </div>
+            </Example>
+          </Section>
+
+
+          <Section name="UnsupportedBrowserView">
+            <Example summary="Standalone Unsupported Browser">
+              <div className="standalone">
+                <UnsupportedBrowserView />
+              </div>
+            </Example>
+          </Section>
+
+          <Section name="UnsupportedDeviceView">
+            <Example summary="Standalone Unsupported Device">
+              <div className="standalone">
+                <UnsupportedDeviceView />
+              </div>
+            </Example>
+          </Section>
+
         </ShowCase>
       );
     }
   });
 
   /**
    * Render components that have different styles across
    * CSS media rules in their own iframe to mimic the viewport