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 225342 6858e2dbf7a5229d661a8d1565c022ca69444ff3
parent 225341 8d53b9d6fc24043372253a86f9e9777720b68d61
child 225343 09e683df2115fbefc11dad146601e15728c96a8d
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault
bugs1065608
milestone34.0a2
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