Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 20 Jan 2015 20:49:09 -0500
changeset 224687 0bca66c907ce7a9a065e80fd64629f33fa5d476f
parent 224667 b736f3018a058ce016d6e4e4c5f335a054ada67f (current diff)
parent 224686 9a389eb9609fcc133a7c9a41a755afaadf02783c (diff)
child 224773 92dcd34c8aa12103e0173645f09d6fa0dacfa9fd
push id28142
push userryanvm@gmail.com
push dateWed, 21 Jan 2015 01:49:16 +0000
treeherdermozilla-central@0bca66c907ce [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.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
Merge fx-team to m-c. a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1429,16 +1429,19 @@ pref("devtools.timeline.hiddenMarkers", 
 
 pref("devtools.performance.ui.show-timeline-memory", false);
 
 // The default Profiler UI settings
 pref("devtools.profiler.ui.flatten-tree-recursion", true);
 pref("devtools.profiler.ui.show-platform-data", false);
 pref("devtools.profiler.ui.show-idle-blocks", true);
 
+// The default Performance UI settings
+pref("devtools.performance.ui.invert-call-tree", true);
+
 // The default cache UI setting
 pref("devtools.cache.disabled", false);
 
 // Enable the Network Monitor
 pref("devtools.netmonitor.enabled", true);
 
 // The default Network Monitor UI settings
 pref("devtools.netmonitor.panes-network-details-width", 550);
--- a/browser/base/content/browser-ctrlTab.js
+++ b/browser/base/content/browser-ctrlTab.js
@@ -179,22 +179,30 @@ var ctrlTab = {
       keys[key] = document.getElementById("key_" + key)
                           .getAttribute("key")
                           .toLocaleLowerCase().charCodeAt(0);
     });
     delete this.keys;
     return this.keys = keys;
   },
   _selectedIndex: 0,
-  get selected () this._selectedIndex < 0 ?
-                    document.activeElement :
-                    this.previews.item(this._selectedIndex),
-  get isOpen   () this.panel.state == "open" || this.panel.state == "showing" || this._timer,
-  get tabCount () this.tabList.length,
-  get tabPreviewCount () Math.min(this.maxTabPreviews, this.tabCount),
+  get selected () {
+    return this._selectedIndex < 0 ?
+             document.activeElement :
+             this.previews.item(this._selectedIndex);
+  },
+  get isOpen () {
+    return this.panel.state == "open" || this.panel.state == "showing" || this._timer;
+  },
+  get tabCount () {
+    return this.tabList.length;
+  },
+  get tabPreviewCount () {
+    return Math.min(this.maxTabPreviews, this.tabCount);
+  },
 
   get tabList () {
     return this._recentlyUsedTabs;
   },
 
   init: function ctrlTab_init() {
     if (!this._recentlyUsedTabs) {
       tabPreviews.init();
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -6,16 +6,18 @@
 
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
@@ -765,18 +767,18 @@ loop.conversationViews = (function(mozL1
       }
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
-      if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "user-unknown") {
+      if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
+          callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -6,16 +6,18 @@
 
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
@@ -765,18 +767,18 @@ loop.conversationViews = (function(mozL1
       }
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
-      if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "user-unknown") {
+      if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
+          callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -6,25 +6,21 @@
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
 loop.store.ActiveRoomStore = (function() {
   "use strict";
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
   // Error numbers taken from
   // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
-  var SERVER_CODES = loop.store.SERVER_CODES = {
-    INVALID_TOKEN: 105,
-    EXPIRED: 111,
-    ROOM_FULL: 202
-  };
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   /**
    * Active room store.
    *
    * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
    *                                      and registering to consume actions.
    * @param {Object} options Options object:
@@ -79,33 +75,33 @@ loop.store.ActiveRoomStore = (function()
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
     roomFailure: function(actionData) {
       function getReason(serverCode) {
         switch (serverCode) {
-          case SERVER_CODES.INVALID_TOKEN:
-          case SERVER_CODES.EXPIRED:
-            return FAILURE_REASONS.EXPIRED_OR_INVALID;
+          case REST_ERRNOS.INVALID_TOKEN:
+          case REST_ERRNOS.EXPIRED:
+            return FAILURE_DETAILS.EXPIRED_OR_INVALID;
           default:
-            return FAILURE_REASONS.UNKNOWN;
+            return FAILURE_DETAILS.UNKNOWN;
         }
       }
 
       console.error("Error in state `" + this._storeState.roomState + "`:",
         actionData.error);
 
       this.setStoreState({
         error: actionData.error,
         failureReason: getReason(actionData.error.errno)
       });
 
-      this._leaveRoom(actionData.error.errno === SERVER_CODES.ROOM_FULL ?
+      this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
           ROOM_STATES.FULL : ROOM_STATES.FAILED);
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in after the initial setup has been performed.
      */
     _registerPostSetupActions: function() {
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -6,16 +6,17 @@
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
 (function() {
   var sharedActions = loop.shared.actions;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   /**
    * Websocket states taken from:
    * https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
    */
   var WS_STATES = loop.store.WS_STATES = {
     // The call is starting, and the remote party is not yet being alerted.
     INIT: "init",
     // The called party is being alerted.
@@ -370,18 +371,18 @@ loop.store = loop.store || {};
       appendContactValues("tel", true);
 
       this.client.setupOutgoingCall(contactAddresses,
         this.getStoreState("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
             var failureReason = "setup";
-            if (err.errno == 122) {
-              failureReason = "user-unknown";
+            if (err.errno == REST_ERRNOS.USER_UNAVAILABLE) {
+              failureReason = REST_ERRNOS.USER_UNAVAILABLE;
             }
             this.dispatcher.dispatch(
               new sharedActions.ConnectionFailure({reason: failureReason}));
             return;
           }
 
           // Success, dispatch a new action.
           this.dispatcher.dispatch(
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.OTSdkDriver = (function() {
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
   /**
    * This is a wrapper for the OT sdk. It is used to translate the SDK events into
    * actions, and instruct the SDK what to do as a result of actions.
    */
   var OTSdkDriver = function(options) {
       if (!options.dispatcher) {
         throw new Error("Missing option dispatcher");
@@ -155,17 +155,17 @@ loop.OTSdkDriver = (function() {
      * Called once the session has finished connecting.
      *
      * @param {Error} error An OT error object, null if there was no error.
      */
     _onConnectionComplete: function(error) {
       if (error) {
         console.error("Failed to complete connection", error);
         this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-          reason: FAILURE_REASONS.COULD_NOT_CONNECT
+          reason: FAILURE_DETAILS.COULD_NOT_CONNECT
         }));
         return;
       }
 
       this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers());
       this._sessionConnected = true;
       this._maybePublishLocalStream();
     },
@@ -192,20 +192,20 @@ loop.OTSdkDriver = (function() {
      *
      * @param {SessionDisconnectEvent} event The event details:
      * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
      */
     _onSessionDisconnected: function(event) {
       var reason;
       switch (event.reason) {
         case "networkDisconnected":
-          reason = FAILURE_REASONS.NETWORK_DISCONNECTED;
+          reason = FAILURE_DETAILS.NETWORK_DISCONNECTED;
           break;
         case "forceDisconnected":
-          reason = FAILURE_REASONS.EXPIRED_OR_INVALID;
+          reason = FAILURE_DETAILS.EXPIRED_OR_INVALID;
           break;
         default:
           // Other cases don't need to be handled.
           return;
       }
 
       this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
         reason: reason
@@ -273,17 +273,17 @@ loop.OTSdkDriver = (function() {
      *
      * @param {OT.Event} event
      */
     _onPublishDenied: function(event) {
       // This prevents the SDK's "access denied" dialog showing.
       event.preventDefault();
 
       this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-        reason: FAILURE_REASONS.MEDIA_DENIED
+        reason: FAILURE_DETAILS.MEDIA_DENIED
       }));
     },
 
     /**
      * Publishes the local stream if the session is connected
      * and the publisher is ready.
      */
     _maybePublishLocalStream: function() {
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -12,17 +12,34 @@ loop.shared.utils = (function(mozL10n) {
   /**
    * Call types used for determining if a call is audio/video or audio-only.
    */
   var CALL_TYPES = {
     AUDIO_VIDEO: "audio-video",
     AUDIO_ONLY: "audio"
   };
 
-  var FAILURE_REASONS = {
+  var REST_ERRNOS = {
+    INVALID_TOKEN: 105,
+    EXPIRED: 111,
+    USER_UNAVAILABLE: 122,
+    ROOM_FULL: 202
+  };
+
+  var WEBSOCKET_REASONS = {
+    ANSWERED_ELSEWHERE: "answered-elsewhere",
+    BUSY: "busy",
+    CANCEL: "cancel",
+    CLOSED: "closed",
+    MEDIA_FAIL: "media-fail",
+    REJECT: "reject",
+    TIMEOUT: "timeout"
+  };
+
+  var FAILURE_DETAILS = {
     MEDIA_DENIED: "reason-media-denied",
     COULD_NOT_CONNECT: "reason-could-not-connect",
     NETWORK_DISCONNECTED: "reason-network-disconnected",
     EXPIRED_OR_INVALID: "reason-expired-or-invalid",
     UNKNOWN: "reason-unknown"
   };
 
   /**
@@ -113,15 +130,17 @@ loop.shared.utils = (function(mozL10n) {
         learnMoreUrl: navigator.mozLoop.getLoopPref("learnMoreUrl")
       }),
       recipient
     );
   }
 
   return {
     CALL_TYPES: CALL_TYPES,
-    FAILURE_REASONS: FAILURE_REASONS,
+    FAILURE_DETAILS: FAILURE_DETAILS,
+    REST_ERRNOS: REST_ERRNOS,
+    WEBSOCKET_REASONS: WEBSOCKET_REASONS,
     Helper: Helper,
     composeCallUrlEmail: composeCallUrlEmail,
     formatDate: formatDate,
     getBoolPreference: getBoolPreference
   };
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.CallConnectionWebSocket = (function() {
   "use strict";
 
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
+
   // Response timeout is 5 seconds as per API.
   var kResponseTimeout = 5000;
 
   /**
    * Handles a websocket specifically for a call connection.
    *
    * There should be one of these created for each call connection.
    *
@@ -61,17 +63,17 @@ loop.CallConnectionWebSocket = (function
           this.socket = new WebSocket(this.options.url);
           this.socket.onopen = this._onopen.bind(this);
           this.socket.onmessage = this._onmessage.bind(this);
           this.socket.onerror = this._onerror.bind(this);
           this.socket.onclose = this._onclose.bind(this);
 
           var timeout = setTimeout(function() {
             if (this.connectDetails && this.connectDetails.reject) {
-              this.connectDetails.reject("timeout");
+              this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
               this._clearConnectionFlags();
             }
           }.bind(this), kResponseTimeout);
           this.connectDetails = {
             resolve: resolve,
             reject: reject,
             timeout: timeout
           };
@@ -134,17 +136,17 @@ loop.CallConnectionWebSocket = (function
 
     /**
      * Notifies the server that the user has declined the call.
      */
     decline: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "reject"
+        reason: WEBSOCKET_REASONS.REJECT
       });
     },
 
     /**
      * Notifies the server that the user has accepted the call.
      */
     accept: function() {
       this._send({
@@ -167,28 +169,28 @@ loop.CallConnectionWebSocket = (function
     /**
      * Notifies the server that the outgoing call is cancelled by the
      * user.
      */
     cancel: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "cancel"
+        reason: WEBSOCKET_REASONS.CANCEL
       });
     },
 
     /**
      * Notifies the server that something failed during setup.
      */
     mediaFail: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "media-fail"
+        reason: WEBSOCKET_REASONS.MEDIA_FAIL
       });
     },
 
     /**
      * Sends data on the websocket.
      *
      * @param {Object} data The data to send.
      */
@@ -223,36 +225,36 @@ loop.CallConnectionWebSocket = (function
     },
 
     /**
      * Called when a message is received from the server.
      *
      * @param {Object} event The websocket onmessage event.
      */
     _onmessage: function(event) {
-      var msg;
+      var msgData;
       try {
-        msg = JSON.parse(event.data);
+        msgData = JSON.parse(event.data);
       } catch (x) {
         console.error("Error parsing received message:", x);
         return;
       }
 
       this._log("WS Receiving", event.data);
 
       var previousState = this._lastServerState;
-      this._lastServerState = msg.state;
+      this._lastServerState = msgData.state;
 
-      switch(msg.messageType) {
+      switch(msgData.messageType) {
         case "hello":
-          this._completeConnection(msg.state);
+          this._completeConnection(msgData.state);
           break;
         case "progress":
-          this.trigger("progress:" + msg.state);
-          this.trigger("progress", msg, previousState);
+          this.trigger("progress:" + msgData.state);
+          this.trigger("progress", msgData, previousState);
           break;
       }
     },
 
     /**
      * Called when there is an error on the websocket.
      *
      * @param {Object} event A simple error event.
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -6,17 +6,17 @@
 
 /* global loop:true, React */
 /* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
@@ -54,19 +54,19 @@ loop.standaloneRoomViews = (function(moz
       );
     },
 
     /**
      * @return String An appropriate string according to the failureReason.
      */
     _getFailureString: function() {
       switch(this.props.failureReason) {
-        case FAILURE_REASONS.MEDIA_DENIED:
+        case FAILURE_DETAILS.MEDIA_DENIED:
           return mozL10n.get("rooms_media_denied_message");
-        case FAILURE_REASONS.EXPIRED_OR_INVALID:
+        case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     render: function() {
       switch(this.props.roomState) {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -6,17 +6,17 @@
 
 /* global loop:true, React */
 /* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
@@ -54,19 +54,19 @@ loop.standaloneRoomViews = (function(moz
       );
     },
 
     /**
      * @return String An appropriate string according to the failureReason.
      */
     _getFailureString: function() {
       switch(this.props.failureReason) {
-        case FAILURE_REASONS.MEDIA_DENIED:
+        case FAILURE_DETAILS.MEDIA_DENIED:
           return mozL10n.get("rooms_media_denied_message");
-        case FAILURE_REASONS.EXPIRED_OR_INVALID:
+        case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     render: function() {
       switch(this.props.roomState) {
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -14,16 +14,17 @@ loop.webapp = (function($, _, OT, mozL10
   loop.config = loop.config || {};
   loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
 
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedViews = loop.shared.views;
   var sharedUtils = loop.shared.utils;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   var multiplexGum = loop.standaloneMedia.multiplexGum;
 
   /**
    * Homepage view.
    */
   var HomeView = React.createClass({
     render: function() {
@@ -887,17 +888,17 @@ loop.webapp = (function($, _, OT, mozL10
      * Handles call rejection.
      *
      * @param {String} reason The reason the call was terminated (reject, busy,
      *                        timeout, cancel, media-fail, user-unknown, closed)
      */
     _handleCallTerminated: function(reason) {
       multiplexGum.reset();
 
-      if (reason === "cancel") {
+      if (reason === WEBSOCKET_REASONS.CANCEL) {
         this.setState({callStatus: "start"});
         return;
       }
       // XXX later, we'll want to display more meaningfull messages (needs UX)
       this.props.notifications.errorL10n("call_timeout_notification_text");
       this.setState({callStatus: "failure"});
     },
 
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -7,16 +7,18 @@ describe("loop.conversationViews", funct
   "use strict";
 
   var sharedUtils = loop.shared.utils;
   var sharedView = loop.shared.views;
   var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR;
   var fakeMozLoop, fakeWindow;
 
   var CALL_STATES = loop.store.CALL_STATES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   // XXX refactor to Just Work with "sandbox.stubComponent" or else
   // just pass in the sandbox and put somewhere generally usable
 
   function stubComponent(obj, component, mockTagName){
     var reactClass = React.createClass({
       render: function() {
         var mockTagName = mockTagName || "div";
@@ -410,39 +412,39 @@ describe("loop.conversationViews", funct
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
 
-    it("should show 'something went wrong' when the reason is 'media-fail'",
+    it("should show 'something went wrong' when the reason is WEBSOCKET_REASONS.MEDIA_FAIL",
       function () {
-        store.setStoreState({callStateReason: "media-fail"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWith(document.mozL10n.get, "generic_failure_title");
       });
 
-    it("should show 'contact unavailable' when the reason is 'reject'",
+    it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.REJECT",
       function () {
-        store.setStoreState({callStateReason: "reject"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.REJECT});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
-    it("should show 'contact unavailable' when the reason is 'busy'",
+    it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.BUSY",
       function () {
-        store.setStoreState({callStateReason: "busy"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
@@ -451,30 +453,30 @@ describe("loop.conversationViews", funct
         store.setStoreState({callStateReason: "setup"});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "generic_failure_title");
       });
 
-    it("should show 'contact unavailable' when the reason is 'user-unknown'",
+    it("should show 'contact unavailable' when the reason is REST_ERRNOS.USER_UNAVAILABLE",
       function () {
-        store.setStoreState({callStateReason: "user-unknown"});
+        store.setStoreState({callStateReason: REST_ERRNOS.USER_UNAVAILABLE});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
     it("should display a generic contact unavailable msg when the reason is" +
-       " 'busy' and no display name is available", function() {
-        store.setStoreState({callStateReason: "busy"});
+       " WEBSOCKET_REASONS.BUSY and no display name is available", function() {
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
         var phoneOnlyContact = {
           tel: [{"pref": true, type: "work", value: ""}]
         };
 
         view = mountTestComponent({contact: phoneOnlyContact});
 
         sinon.assert.calledWith(document.mozL10n.get,
           "generic_contact_unavailable_title");
@@ -853,41 +855,41 @@ describe("loop.conversationViews", funct
             sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
           });
 
           describe("progress - terminated (previousState = alerting)", function() {
             it("should stop alerting", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "timeout"
+                  reason: WEBSOCKET_REASONS.TIMEOUT
                 }, "alerting");
 
                 sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                 done();
               });
             });
 
             it("should close the websocket", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "closed"
+                  reason: WEBSOCKET_REASONS.CLOSED
                 }, "alerting");
 
                 sinon.assert.calledOnce(icView._websocket.close);
                 done();
               });
             });
 
             it("should close the window", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "answered-elsewhere"
+                  reason: WEBSOCKET_REASONS.ANSWERED_ELSEWHERE
                 }, "alerting");
 
                 sandbox.clock.tick(1);
 
                 sinon.assert.calledOnce(fakeWindow.close);
                 done();
               });
             });
@@ -896,29 +898,29 @@ describe("loop.conversationViews", funct
 
           describe("progress - terminated (previousState not init" +
                    " nor alerting)",
             function() {
               it("should set the state to end", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "media-fail"
+                    reason: WEBSOCKET_REASONS.MEDIA_FAIL
                   }, "connecting");
 
                   expect(icView.state.callStatus).eql("end");
                   done();
                 });
               });
 
               it("should stop alerting", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "media-fail"
+                    reason: WEBSOCKET_REASONS.MEDIA_FAIL
                   }, "connecting");
 
                   sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                   done();
                 });
               });
             });
         });
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -1,19 +1,19 @@
 /* global chai, loop */
 
 var expect = chai.expect;
 var sharedActions = loop.shared.actions;
 
 describe("loop.store.ActiveRoomStore", function () {
   "use strict";
 
-  var SERVER_CODES = loop.store.SERVER_CODES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
   var fakeMultiplexGum;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
@@ -89,48 +89,48 @@ describe("loop.store.ActiveRoomStore", f
       store.roomFailure({error: fakeError});
 
       sinon.assert.calledOnce(console.error);
       sinon.assert.calledWith(console.error,
         sinon.match(ROOM_STATES.JOINED), fakeError);
     });
 
     it("should set the state to `FULL` on server error room full", function() {
-      fakeError.errno = SERVER_CODES.ROOM_FULL;
+      fakeError.errno = REST_ERRNOS.ROOM_FULL;
 
       store.roomFailure({error: fakeError});
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
     });
 
     it("should set the state to `FAILED` on generic error", function() {
       store.roomFailure({error: fakeError});
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-      expect(store._storeState.failureReason).eql(FAILURE_REASONS.UNKNOWN);
+      expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
     });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "invalid token", function() {
-        fakeError.errno = SERVER_CODES.INVALID_TOKEN;
+        fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
 
         store.roomFailure({error: fakeError});
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-        expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID);
+        expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "expired", function() {
-        fakeError.errno = SERVER_CODES.EXPIRED;
+        fakeError.errno = REST_ERRNOS.EXPIRED;
 
         store.roomFailure({error: fakeError});
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-        expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID);
+        expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should reset the multiplexGum", function() {
       store.roomFailure({error: fakeError});
 
       sinon.assert.calledOnce(fakeMultiplexGum.reset);
     });
 
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -3,16 +3,17 @@
 
 var expect = chai.expect;
 
 describe("loop.store.ConversationStore", function () {
   "use strict";
 
   var CALL_STATES = loop.store.CALL_STATES;
   var WS_STATES = loop.store.WS_STATES;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
   var contact, fakeMozLoop;
   var connectPromise, resolveConnectPromise, rejectConnectPromise;
   var wsCancelSpy, wsCloseSpy, wsMediaUpSpy, fakeWebsocket;
 
   function checkFailures(done, f) {
@@ -754,25 +755,25 @@ describe("loop.store.ConversationStore",
           new sharedActions.ConnectCall({sessionData: fakeSessionData}));
 
         sandbox.stub(dispatcher, "dispatch");
       });
 
       it("should dispatch a connection failure action on 'terminate'", function() {
         store._websocket.trigger("progress", {
           state: WS_STATES.TERMINATED,
-          reason: "reject"
+          reason: WEBSOCKET_REASONS.REJECT
         });
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         // Can't use instanceof here, as that matches any action
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", "reject"));
+          sinon.match.hasOwn("reason", WEBSOCKET_REASONS.REJECT));
       });
 
       it("should dispatch a connection progress action on 'alerting'", function() {
         store._websocket.trigger("progress", {state: WS_STATES.ALERTING});
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         // Can't use instanceof here, as that matches any action
         sinon.assert.calledWithMatch(dispatcher.dispatch,
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.OTSdkDriver", function () {
   "use strict";
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sandbox;
   var dispatcher, driver, publisher, sdk, session, sessionData;
   var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     fakeLocalElement = {fake: 1};
@@ -148,17 +148,17 @@ describe("loop.OTSdkDriver", function ()
         sandbox.stub(dispatcher, "dispatch");
 
         driver.connectSession(sessionData);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", FAILURE_REASONS.COULD_NOT_CONNECT));
+          sinon.match.hasOwn("reason", FAILURE_DETAILS.COULD_NOT_CONNECT));
       });
     });
   });
 
   describe("#disconnectionSession", function() {
     it("should disconnect the session", function() {
       driver.session = session;
 
@@ -260,30 +260,30 @@ describe("loop.OTSdkDriver", function ()
           session.trigger("sessionDisconnected", {
             reason: "networkDisconnected"
           });
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "connectionFailure"));
           sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_REASONS.NETWORK_DISCONNECTED));
+            sinon.match.hasOwn("reason", FAILURE_DETAILS.NETWORK_DISCONNECTED));
         });
 
       it("should dispatch a connectionFailure action if the session was " +
          "forcibly disconnected", function() {
           session.trigger("sessionDisconnected", {
             reason: "forceDisconnected"
           });
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "connectionFailure"));
           sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_REASONS.EXPIRED_OR_INVALID));
+            sinon.match.hasOwn("reason", FAILURE_DETAILS.EXPIRED_OR_INVALID));
         });
     });
 
     describe("streamCreated", function() {
       var fakeStream;
 
       beforeEach(function() {
         fakeStream = {
@@ -371,17 +371,17 @@ describe("loop.OTSdkDriver", function ()
 
       it("should dispatch connectionFailure", function() {
         publisher.trigger("accessDenied", fakeEvent);
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", FAILURE_REASONS.MEDIA_DENIED));
+          sinon.match.hasOwn("reason", FAILURE_DETAILS.MEDIA_DENIED));
       });
     });
 
     describe("accessDialogOpened", function() {
       it("should prevent the default event behavior", function() {
         publisher.trigger("accessDialogOpened", fakeEvent);
 
         sinon.assert.calledOnce(fakeEvent.preventDefault);
--- a/browser/components/loop/test/shared/websocket_test.js
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -1,19 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*global loop, sinon, it, beforeEach, afterEach, describe */
 
 var expect = chai.expect;
 
+
 describe("loop.CallConnectionWebSocket", function() {
   "use strict";
 
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
+
   var sandbox,
       dummySocket;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dummySocket = {
@@ -75,17 +78,17 @@ describe("loop.CallConnectionWebSocket",
 
       it("should reject the promise if connection is not completed in " +
          "5 seconds", function(done) {
         var promise = callWebSocket.promiseConnect();
 
         sandbox.clock.tick(5101);
 
         promise.then(function() {}, function(error) {
-          expect(error).to.be.equal("timeout");
+          expect(error).to.be.equal(WEBSOCKET_REASONS.TIMEOUT);
           done();
         });
       });
 
       it("should reject the promise if the connection errors", function(done) {
         var promise = callWebSocket.promiseConnect();
 
         dummySocket.onerror("error");
@@ -152,17 +155,17 @@ describe("loop.CallConnectionWebSocket",
         callWebSocket.promiseConnect();
 
         callWebSocket.decline();
 
         sinon.assert.calledOnce(dummySocket.send);
         sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
           messageType: "action",
           event: "terminate",
-          reason: "reject"
+          reason: WEBSOCKET_REASONS.REJECT
         }));
       });
     });
 
     describe("#accept", function() {
       it("should send an accept message to the server", function() {
         callWebSocket.promiseConnect();
 
@@ -186,60 +189,60 @@ describe("loop.CallConnectionWebSocket",
         sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
           messageType: "action",
           event: "media-up"
         }));
       });
     });
 
     describe("#cancel", function() {
-      it("should send a terminate message to the server with a reason of cancel",
+      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.CANCEL",
         function() {
           callWebSocket.promiseConnect();
 
           callWebSocket.cancel();
 
           sinon.assert.calledOnce(dummySocket.send);
           sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
             messageType: "action",
             event: "terminate",
-            reason: "cancel"
+            reason: WEBSOCKET_REASONS.CANCEL
           }));
         });
     });
 
     describe("#mediaFail", function() {
-      it("should send a terminate message to the server with a reason of media-fail",
+      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.MEDIA_FAIL",
         function() {
           callWebSocket.promiseConnect();
 
           callWebSocket.mediaFail();
 
           sinon.assert.calledOnce(dummySocket.send);
           sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
             messageType: "action",
             event: "terminate",
-            reason: "media-fail"
+            reason: WEBSOCKET_REASONS.MEDIA_FAIL
           }));
         });
     });
 
     describe("Events", function() {
       beforeEach(function() {
         sandbox.stub(callWebSocket, "trigger");
 
         callWebSocket.promiseConnect();
       });
 
       describe("Progress", function() {
         it("should trigger a progress event on the callWebSocket", function() {
           var eventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(eventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
@@ -256,33 +259,33 @@ describe("loop.CallConnectionWebSocket",
           // ready for the main test below.
           dummySocket.onmessage({
             data: JSON.stringify(previousEventData)
           });
 
           var currentEventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(currentEventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
                                          currentEventData, "alerting");
         });
 
         it("should trigger a progress:<state> event on the callWebSocket", function() {
           var eventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(eventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -15,17 +15,18 @@ describe("loop.webapp", function() {
       sharedViews = loop.shared.views,
       sharedUtils = loop.shared.utils,
       standaloneMedia = loop.standaloneMedia,
       sandbox,
       notifications,
       stubGetPermsAndCacheMedia,
       fakeAudioXHR,
       dispatcher,
-      feedbackStore;
+      feedbackStore,
+      WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
     feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: {}
     });
@@ -237,54 +238,54 @@ describe("loop.webapp", function() {
             beforeEach(function() {
               sandbox.stub(notifications, "errorL10n");
               sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
             });
 
             it("should display the FailedConversationView", function() {
               ocView._websocket.trigger("progress", {
                 state: "terminated",
-                reason: "reject"
+                reason: WEBSOCKET_REASONS.REJECT
               });
 
               TestUtils.findRenderedComponentWithType(ocView,
                 loop.webapp.FailedConversationView);
             });
 
             it("should reset multiplexGum when a call is rejected",
               function() {
                 var multiplexGum = new standaloneMedia._MultiplexGum();
                 standaloneMedia.setSingleton(multiplexGum);
                 sandbox.stub(standaloneMedia._MultiplexGum.prototype, "reset");
 
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "reject"
+                  reason: WEBSOCKET_REASONS.REJECT
                 });
 
                 sinon.assert.calledOnce(multiplexGum.reset);
               });
 
-            it("should display an error message if the reason is not 'cancel'",
+            it("should display an error message if the reason is not WEBSOCKET_REASONS.CANCEL",
               function() {
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "reject"
+                  reason: WEBSOCKET_REASONS.REJECT
                 });
 
                 sinon.assert.calledOnce(notifications.errorL10n);
                 sinon.assert.calledWithExactly(notifications.errorL10n,
                   "call_timeout_notification_text");
               });
 
-            it("should not display an error message if the reason is 'cancel'",
+            it("should not display an error message if the reason is WEBSOCKET_REASONS.CANCEL",
               function() {
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "cancel"
+                  reason: WEBSOCKET_REASONS.CANCEL
                 });
 
                 sinon.assert.notCalled(notifications.errorL10n);
               });
           });
 
           describe("state: connecting", function() {
             it("should set display the ConversationView", function() {
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -5,19 +5,17 @@ support-files =
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   browser_toolbox_sidebar_tool.xul
   head.js
   helper_disable_cache.js
   doc_theme.css
 
 [browser_devtools_api.js]
-skip-if = e10s # Bug 1090340
 [browser_devtools_api_destroy.js]
-skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
 [browser_dynamic_tool_enabling.js]
 [browser_keybindings.js]
 [browser_new_activation_workflow.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_two_tabs.js]
 [browser_toolbox_dynamic_registration.js]
@@ -27,17 +25,17 @@ skip-if = e10s # Bug 1070837 - devtools/
 [browser_toolbox_options.js]
 [browser_toolbox_options_devedition.js]
 [browser_toolbox_options_disable_buttons.js]
 [browser_toolbox_options_disable_cache-01.js]
 skip-if = e10s # Bug 1030318
 [browser_toolbox_options_disable_cache-02.js]
 skip-if = e10s # Bug 1030318
 [browser_toolbox_options_disable_js.js]
-skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
+skip-if = e10s # Bug 1030318
 # [browser_toolbox_raise.js] # Bug 962258
 # skip-if = os == "win"
 [browser_toolbox_ready.js]
 [browser_toolbox_select_event.js]
 skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
 [browser_toolbox_sidebar.js]
 [browser_toolbox_sidebar_events.js]
 [browser_toolbox_sidebar_existing_tabs.js]
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -91,16 +91,17 @@ browser.jar:
     content/browser/devtools/profiler.js                               (profiler/profiler.js)
     content/browser/devtools/ui-recordings.js                          (profiler/ui-recordings.js)
     content/browser/devtools/ui-profile.js                             (profiler/ui-profile.js)
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
     content/browser/devtools/performance.xul                           (performance/performance.xul)
     content/browser/devtools/performance/performance-controller.js     (performance/performance-controller.js)
     content/browser/devtools/performance/performance-view.js           (performance/performance-view.js)
     content/browser/devtools/performance/views/overview.js             (performance/views/overview.js)
+    content/browser/devtools/performance/views/toolbar.js              (performance/views/toolbar.js)
     content/browser/devtools/performance/views/details.js              (performance/views/details.js)
     content/browser/devtools/performance/views/details-call-tree.js    (performance/views/details-call-tree.js)
     content/browser/devtools/performance/views/details-waterfall.js    (performance/views/details-waterfall.js)
     content/browser/devtools/performance/views/details-flamegraph.js   (performance/views/details-flamegraph.js)
     content/browser/devtools/performance/views/recordings.js           (performance/views/recordings.js)
 #endif
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -39,30 +39,37 @@ devtools.lazyRequireGetter(this, "Waterf
 devtools.lazyRequireGetter(this, "MarkerDetails",
   "devtools/timeline/marker-details", true);
 devtools.lazyRequireGetter(this, "CallView",
   "devtools/profiler/tree-view", true);
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/profiler/tree-model", true);
 devtools.lazyRequireGetter(this, "FrameNode",
   "devtools/profiler/tree-model", true);
+devtools.lazyRequireGetter(this, "OptionsView",
+  "devtools/shared/options-view", true);
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "LineGraphWidget",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "FlameGraphUtils",
   "resource:///modules/devtools/FlameGraph.jsm");
 devtools.lazyImporter(this, "FlameGraph",
   "resource:///modules/devtools/FlameGraph.jsm");
 devtools.lazyImporter(this, "SideMenuWidget",
   "resource:///modules/devtools/SideMenuWidget.jsm");
 
+const BRANCH_NAME = "devtools.performance.ui.";
+
 // Events emitted by various objects in the panel.
 const EVENTS = {
+  // Fired by the OptionsView when a preference changes.
+  PREF_CHANGED: "Preformance:PrefChanged",
+
   // Emitted by the PerformanceController or RecordingView
   // when a recording model is selected
   RECORDING_SELECTED: "Performance:RecordingSelected",
 
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
 
@@ -161,53 +168,64 @@ let PrefObserver = {
 let PerformanceController = {
   _recordings: [],
   _currentRecording: null,
 
   /**
    * Listen for events emitted by the current tab target and
    * main UI events.
    */
-  initialize: function() {
+  initialize: Task.async(function* () {
     this.startRecording = this.startRecording.bind(this);
     this.stopRecording = this.stopRecording.bind(this);
     this.importRecording = this.importRecording.bind(this);
     this.exportRecording = this.exportRecording.bind(this);
     this._onTimelineData = this._onTimelineData.bind(this);
     this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
+    this._onPrefChanged = this._onPrefChanged.bind(this);
 
+    ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gFront.on("ticks", this._onTimelineData); // framerate
     gFront.on("markers", this._onTimelineData); // timeline markers
     gFront.on("frames", this._onTimelineData); // stack frames
     gFront.on("memory", this._onTimelineData); // timeline memory
-  },
+  }),
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
+    ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gFront.off("ticks", this._onTimelineData);
     gFront.off("markers", this._onTimelineData);
     gFront.off("frames", this._onTimelineData);
     gFront.off("memory", this._onTimelineData);
   },
 
   /**
+   * Get a preference setting from `prefName` via the underlying
+   * OptionsView in the ToolbarView.
+   */
+  getPref: function (prefName) {
+    return ToolbarView.optionsView.getPref(prefName);
+  },
+
+  /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
     let recording = this.createNewRecording();
     this.setCurrentRecording(recording);
     yield recording.startRecording();
 
@@ -310,16 +328,24 @@ let PerformanceController = {
   },
 
   /**
    * Fired from RecordingsView, we listen on the PerformanceController so we can
    * set it here and re-emit on the controller, where all views can listen.
    */
   _onRecordingSelectFromView: function (_, recording) {
     this.setCurrentRecording(recording);
+  },
+
+  /**
+   * Fired when the ToolbarView fires a PREF_CHANGED event.
+   * with the value.
+   */
+  _onPrefChanged: function (_, prefName, value) {
+    this.emit(EVENTS.PREF_CHANGED, prefName, value);
   }
 };
 
 /**
  * Convenient way of emitting events from the controller.
  */
 EventEmitter.decorate(PerformanceController);
 
--- a/browser/devtools/performance/performance-view.js
+++ b/browser/devtools/performance/performance-view.js
@@ -24,16 +24,17 @@ let PerformanceView = {
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
 
     return promise.all([
       RecordingsView.initialize(),
       OverviewView.initialize(),
+      ToolbarView.initialize(),
       DetailsView.initialize()
     ]);
   },
 
   /**
    * Unbinds events and destroys subviews.
    */
   destroy: function () {
@@ -41,16 +42,17 @@ let PerformanceView = {
     this._importButton.removeEventListener("click", this._onImportButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
 
     return promise.all([
       RecordingsView.destroy(),
       OverviewView.destroy(),
+      ToolbarView.destroy(),
       DetailsView.destroy()
     ]);
   },
 
   /**
    * Adds the `locked` attribute on the record button. This prevents it
    * from being clicked while recording is started or stopped.
    */
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -13,22 +13,32 @@
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="application/javascript" src="performance/performance-controller.js"/>
   <script type="application/javascript" src="performance/performance-view.js"/>
   <script type="application/javascript" src="performance/recording-model.js"/>
   <script type="application/javascript" src="performance/views/overview.js"/>
+  <script type="application/javascript" src="performance/views/toolbar.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/details-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-waterfall.js"/>
   <script type="application/javascript" src="performance/views/details-flamegraph.js"/>
   <script type="application/javascript" src="performance/views/recordings.js"/>
 
+  <popupset id="performance-options-popupset">
+    <menupopup id="performance-options-menupopup">
+      <menuitem id="option-invert-call-tree"
+                type="checkbox"
+                data-pref="invert-call-tree"
+                label="&profilerUI.invertTree;"
+                tooltiptext="&profilerUI.invertTree.tooltiptext;"/>
+    </menupopup>
+  </popupset>
   <hbox class="theme-body" flex="1">
     <vbox id="recordings-pane">
       <toolbar id="recordings-toolbar"
                class="devtools-toolbar">
         <hbox id="recordings-controls"
               class="devtools-toolbarbutton-group">
           <toolbarbutton id="record-button"
                          class="devtools-toolbarbutton"
@@ -52,16 +62,22 @@
           <toolbarbutton id="select-calltree-view"
                          class="devtools-toolbarbutton"
                          data-view="calltree" />
           <toolbarbutton id="select-flamegraph-view"
                          class="devtools-toolbarbutton"
                          data-view="flamegraph" />
         </hbox>
         <spacer flex="1"></spacer>
+        <hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group">
+          <toolbarbutton id="performance-options-button"
+                         class="devtools-toolbarbutton devtools-option-toolbarbutton"
+                         popup="performance-options-menupopup"
+                         tooltiptext="&profilerUI.options.tooltiptext;"/>
+        </hbox>
       </toolbar>
 
       <vbox id="overview-pane">
         <hbox id="markers-overview"/>
         <hbox id="memory-overview"/>
         <hbox id="time-framerate"/>
       </vbox>
       <deck id="details-pane" flex="1">
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -22,16 +22,17 @@ support-files =
 [browser_perf-front-profiler-02.js]
 [browser_perf-front-profiler-03.js]
 [browser_perf-front-profiler-04.js]
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
 [browser_perf-front.js]
 [browser_perf-jump-to-debugger-01.js]
 [browser_perf-jump-to-debugger-02.js]
+[browser_perf-options-invert-call-tree.js]
 [browser_perf-overview-render-01.js]
 [browser_perf-overview-render-02.js]
 [browser_perf-overview-selection-01.js]
 [browser_perf-overview-selection-02.js]
 [browser_perf-overview-selection-03.js]
 [browser_perf-shared-connection-02.js]
 [browser_perf-shared-connection-03.js]
 # [browser_perf-shared-connection-04.js] bug 1077464
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-options-invert-call-tree.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
+
+/**
+ * Tests that the call tree view renders after recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, CallTreeView } = panel.panelWin;
+
+  Services.prefs.setBoolPref(INVERT_PREF, true);
+
+  yield startRecording(panel);
+  yield busyWait(100);
+
+  let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+  Services.prefs.setBoolPref(INVERT_PREF, false);
+  yield rendered;
+
+  ok(true, "CallTreeView rerendered when toggling invert-call-tree.");
+
+  rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+  Services.prefs.setBoolPref(INVERT_PREF, true);
+  yield rendered;
+
+  ok(true, "CallTreeView rerendered when toggling back invert-call-tree.");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -1,25 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 
-// Enable logging for all the tests. Both the debugger server and frontend will
-// be affected by this pref.
-let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
-Services.prefs.setBoolPref("devtools.debugger.log", false);
-
-// Enable the new performance panel for all tests. Remove this after
-// bug 1075567 is resolved.
-let gToolEnabled = Services.prefs.getBoolPref("devtools.performance_dev.enabled");
-
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
 let { getPerformanceActorsConnection, PerformanceFront } = devtools.require("devtools/performance/front");
 let nsIProfilerModule = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
@@ -30,33 +21,52 @@ const FRAME_SCRIPT_UTILS_URL = "chrome:/
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/performance/test/";
 const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 gDevTools.testing = true;
 
+let DEFAULT_PREFS = [
+  "devtools.debugger.log",
+  "devtools.performance.ui.invert-call-tree",
+  // remove after bug 1075567 is resolved.
+  "devtools.performance_dev.enabled"
+].reduce((prefs, pref) => {
+  prefs[pref] = Services.prefs.getBoolPref(pref);
+  return prefs;
+}, {});
+
+// Enable the new performance panel for all tests. Remove this after
+// bug 1075567 is resolved.
+Services.prefs.setBoolPref("devtools.performance_dev.enabled", true);
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
 /**
  * Call manually in tests that use frame script utils after initializing
  * the tool. Must be called after initializing so we can detect
  * whether or not `content` is a CPOW or not. Call after init but before navigating
  * to different pages.
  */
 function loadFrameScripts () {
   mm = gBrowser.selectedBrowser.messageManager;
   mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
 }
 
 registerCleanupFunction(() => {
   gDevTools.testing = false;
   info("finish() was called, cleaning up...");
 
-  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
-  Services.prefs.setBoolPref("devtools.performance_dev.enabled", gToolEnabled);
+  // Rollback any pref changes
+  Object.keys(DEFAULT_PREFS).forEach(pref => {
+    Services.prefs.setBoolPref(pref, DEFAULT_PREFS[pref]);
+  });
 
   // Make sure the profiler module is stopped when the test finishes.
   nsIProfilerModule.StopProfiler();
 
   Cu.forceGC();
 });
 
 function addTab(aUrl, aWindow) {
@@ -163,17 +173,16 @@ function initPerformance(aUrl, selectedT
   info("Initializing a performance pane.");
 
   return Task.spawn(function*() {
     let tab = yield addTab(aUrl);
     let target = TargetFactory.forTab(tab);
 
     yield target.makeRemote();
 
-    Services.prefs.setBoolPref("devtools.performance_dev.enabled", true);
     let toolbox = yield gDevTools.showToolbox(target, selectedTool);
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
 function* teardown(panel) {
   info("Destroying the performance tool.");
--- a/browser/devtools/performance/views/details-call-tree.js
+++ b/browser/devtools/performance/views/details-call-tree.js
@@ -9,30 +9,33 @@
 let CallTreeView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     this._callTree = $(".call-tree-cells-container");
     this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
     this._onRangeChange = this._onRangeChange.bind(this);
+    this._onPrefChanged = this._onPrefChanged.bind(this);
     this._onLink = this._onLink.bind(this);
 
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
+    PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
+    PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
   },
 
   /**
    * Method for handling all the set up for rendering a new call tree.
    */
   render: function (profilerData, beginAt, endAt, options={}) {
@@ -77,23 +80,27 @@ let CallTreeView = {
       () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
       () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
-  _prepareCallTree: function (profilerData, beginAt, endAt, options) {
+  _prepareCallTree: function (profilerData, startTime, endTime, options) {
     let threadSamples = profilerData.profile.threads[0].samples;
     let contentOnly = !Prefs.showPlatformData;
-    // TODO handle inverted tree bug 1102347
-    let invertTree = false;
+    let invertTree = PerformanceController.getPref("invert-call-tree");
 
-    let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt, invertTree);
+    let threadNode = new ThreadNode(threadSamples,
+      { startTime, endTime, contentOnly, invertTree });
+
+    // If we have an empty profile (no samples), then don't invert the tree, as
+    // it would hide the root node and a completely blank call tree space can be
+    // mis-interpreted as an error.
     options.inverted = invertTree && threadNode.samples > 0;
 
     return threadNode;
   },
 
   /**
    * Renders the call tree.
    */
@@ -109,16 +116,27 @@ let CallTreeView = {
     root.on("link", this._onLink);
 
     // Clear out other call trees.
     this._callTree.innerHTML = "";
     root.attachTo(this._callTree);
 
     let contentOnly = !Prefs.showPlatformData;
     root.toggleCategories(!contentOnly);
+  },
+
+  /**
+   * Called when a preference under "devtools.performance.ui." is changed.
+   */
+  _onPrefChanged: function (_, prefName, value) {
+    if (prefName === "invert-call-tree") {
+      let { beginAt, endAt } = OverviewView.getRange();
+      let profilerData = PerformanceController.getCurrentRecording().getProfilerData();
+      this.render(profilerData, beginAt || void 0, endAt || void 0);
+    }
   }
 };
 
 /**
  * Convenient way of emitting events from the view.
  */
 EventEmitter.decorate(CallTreeView);
 
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -19,16 +19,19 @@ const MEMORY_GRAPH_HEIGHT = 30; // px
 
 const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
 
 /**
  * View handler for the overview panel's time view, displaying
  * framerate, markers and memory over time.
  */
 let OverviewView = {
+  _beginAt: null,
+  _endAt: null,
+
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function *() {
     this._onRecordingStarted = this._onRecordingStarted.bind(this);
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
     this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onRecordingTick = this._onRecordingTick.bind(this);
@@ -64,16 +67,23 @@ let OverviewView = {
 
     clearNamedTimeout("graph-scroll");
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   },
 
   /**
+   * Gets currently selected range's beginAt and endAt values.
+   */
+  getRange: function () {
+    return { beginAt: this._beginAt, endAt: this._endAt };
+  },
+
+  /**
    * Sets up the framerate graph.
    */
   _showFramerateGraph: Task.async(function *() {
     this.framerateGraph = new LineGraphWidget($("#time-framerate"), {
       metric: L10N.getStr("graphs.fps")
     });
     this.framerateGraph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
     yield this.framerateGraph.ready();
@@ -143,18 +153,22 @@ let OverviewView = {
 
   /**
    * Fired when the graph selection has changed. Called by
    * mouseup and scroll events.
    */
   _onSelectionChange: function () {
     if (this.framerateGraph.hasSelection()) {
       let { min: beginAt, max: endAt } = this.framerateGraph.getMappedSelection();
+      this._beginAt = beginAt;
+      this._endAt = endAt;
       this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, { beginAt, endAt });
     } else {
+      this._beginAt = null;
+      this._endAt = null;
       this.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
     }
   },
 
   /**
    * Listener handling the "mouseup" event for the framerate graph.
    * Fires an event to be handled elsewhere.
    */
--- a/browser/devtools/performance/views/recordings.js
+++ b/browser/devtools/performance/views/recordings.js
@@ -93,52 +93,49 @@ let RecordingsView = Heritage.extend(Wid
    */
   _onRecordingStarted: function (_, recording) {
     // Insert a "dummy" recording item, to hint that recording has now started.
     let recordingItem;
 
     // If a label is specified (e.g due to a call to `console.profile`),
     // then try reusing a pre-existing recording item, if there is one.
     // This is symmetrical to how `this.handleRecordingEnded` works.
-    if (recording.getLabel()) {
-      recordingItem = this.getItemForAttachment(e =>
-        e.getLabel() === recording.getLabel());
+    let profileLabel = recording.getLabel();
+    if (profileLabel) {
+      recordingItem = this.getItemForAttachment(e => e.getLabel() == profileLabel);
     }
     // Otherwise, create a new empty recording item.
     if (!recordingItem) {
       recordingItem = this.addEmptyRecording(recording);
     }
 
     // Mark the corresponding item as being a "record in progress".
     recordingItem.isRecording = true;
 
     // If this is a manual recording, immediately select it.
     if (!recording.getLabel()) {
       this.selectedItem = recordingItem;
     }
-
-    this.emit(EVENTS.RECORDING_SELECTED, recording);
   },
 
   /**
    * Signals that a recording session has ended.
    *
    * @param RecordingModel recording
    *        The model of the recording that just stopped.
    */
   _onRecordingStopped: function (_, recording) {
-    let profileLabel = recording.getLabel();
     let recordingItem;
 
     // If a label is specified (e.g due to a call to `console.profileEnd`),
     // then try reusing a pre-existing recording item, if there is one.
     // This is symmetrical to how `this.handleRecordingStarted` works.
+    let profileLabel = recording.getLabel();
     if (profileLabel) {
-      recordingItem = this.getItemForAttachment(e =>
-        e.profilerData.profileLabel == profileLabel);
+      recordingItem = this.getItemForAttachment(e => e.getLabel() == profileLabel);
     }
     // Otherwise, just use the first available recording item.
     if (!recordingItem) {
       recordingItem = this.getItemForPredicate(e => e.isRecording);
     }
 
     // Mark the corresponding item as being a "finished recording".
     recordingItem.isRecording = false;
@@ -158,19 +155,16 @@ let RecordingsView = Heritage.extend(Wid
     let recordingItem = this.addEmptyRecording(model);
     recordingItem.isRecording = false;
 
     // Immediately select the imported recording
     this.selectedItem = recordingItem;
 
     // Render the recording item with finalized information (timing, etc)
     this.finalizeRecording(recordingItem);
-
-    // Fire the selection and allow to propogate.
-    this.emit(EVENTS.RECORDING_SELECTED, model);
   },
 
   /**
    * Adds recording data to a recording item in this container.
    *
    * @param Item recordingItem
    *        An item inserted via `RecordingsView.addEmptyRecording`.
    */
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/toolbar.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * View handler for toolbar events (mostly option toggling and triggering)
+ */
+let ToolbarView = {
+  /**
+   * Sets up the view with event binding.
+   */
+  initialize: Task.async(function *() {
+    this._onPrefChanged = this._onPrefChanged.bind(this);
+
+    this.optionsView = new OptionsView({
+      branchName: BRANCH_NAME,
+      menupopup: $("#performance-options-menupopup")
+    });
+
+    yield this.optionsView.initialize();
+    this.optionsView.on("pref-changed", this._onPrefChanged);
+  }),
+
+  /**
+   * Unbinds events and cleans up view.
+   */
+  destroy: function () {
+    this.optionsView.off("pref-changed", this._onPrefChanged);
+    this.optionsView.destroy();
+  },
+
+  /**
+   * Fired when a preference changes in the underlying OptionsView.
+   * Propogated by the PerformanceController.
+   */
+  _onPrefChanged: function (_, prefName) {
+    let value = Services.prefs.getBoolPref(BRANCH_NAME + prefName);
+    this.emit(EVENTS.PREF_CHANGED, prefName, value);
+  }
+};
+
+EventEmitter.decorate(ToolbarView);
--- a/browser/devtools/profiler/test/browser_profiler_tree-model-03.js
+++ b/browser/devtools/profiler/test/browser_profiler_tree-model-03.js
@@ -6,17 +6,17 @@
  * while at the same time filtering by duration.
  */
 
 function test() {
   let { ThreadNode } = devtools.require("devtools/profiler/tree-model");
 
   // Create a root node from a given samples array, filtering by time.
 
-  let root = new ThreadNode(gSamples, false, 11, 18);
+  let root = new ThreadNode(gSamples, { startTime: 11, endTime: 18 });
 
   // Test the root node.
 
   is(root.duration, 18,
     "The correct duration was calculated for the root node.");
 
   is(Object.keys(root.calls).length, 1,
     "The correct number of child calls were calculated for the root node.");
--- a/browser/devtools/profiler/test/browser_profiler_tree-model-04.js
+++ b/browser/devtools/profiler/test/browser_profiler_tree-model-04.js
@@ -6,17 +6,17 @@
  * while at the same time filtering by duration and content-only frames.
  */
 
 function test() {
   let { ThreadNode } = devtools.require("devtools/profiler/tree-model");
 
   // Create a root node from a given samples array, filtering by time.
 
-  let root = new ThreadNode(gSamples, true, 11, 18);
+  let root = new ThreadNode(gSamples, { startTime: 11, endTime: 18, contentOnly: true });
 
   // Test the root node.
 
   is(root.duration, 18,
     "The correct duration was calculated for the root node.");
 
   is(Object.keys(root.calls).length, 2,
     "The correct number of child calls were calculated for the root node.");
--- a/browser/devtools/profiler/test/browser_profiler_tree-model-05.js
+++ b/browser/devtools/profiler/test/browser_profiler_tree-model-05.js
@@ -40,17 +40,17 @@ let samples = [{
     { location: "B" },
     { location: "F" }
   ]
 }];
 
 function test() {
   let { ThreadNode } = devtools.require("devtools/profiler/tree-model");
 
-  let root = new ThreadNode(samples, undefined, undefined, undefined, true);
+  let root = new ThreadNode(samples, { invertTree: true });
 
   is(Object.keys(root.calls).length, 2,
      "Should get the 2 youngest frames, not the 1 oldest frame");
 
   let C = root.calls.C;
   ok(C, "Should have C as a child of the root.");
 
   is(Object.keys(C.calls).length, 3,
--- a/browser/devtools/profiler/ui-profile.js
+++ b/browser/devtools/profiler/ui-profile.js
@@ -447,30 +447,31 @@ let ProfileView = {
   /**
    * Populates the call tree view in the specified tab's panel with the
    * provided data. The already existing tree will be removed.
    *
    * @param nsIDOMNode panel
    *        The <panel> element in this <tabbox>.
    * @param object profilerData
    *        The data source for this tree.
-   * @param number beginAt
+   * @param number startTime
    *        The earliest time in the data source to start at (in milliseconds).
-   * @param number endAt
+   * @param number endTime
    *        The latest time in the data source to end at (in milliseconds).
    * @param object options
    *        Additional options supported by this operation.
    *        @see ProfileView._populatePanelWidgets
    */
-  _populateCallTree: function(panel, profilerData, beginAt, endAt, options = {}) {
+  _populateCallTree: function(panel, profilerData, startTime, endTime, options = {}) {
     let threadSamples = profilerData.profile.threads[0].samples;
     let contentOnly = !Prefs.showPlatformData;
     let invertChecked = this._invertTree.hasAttribute("checked");
-    let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt,
-                                    invertChecked);
+    let threadNode = new ThreadNode(threadSamples,
+      { startTime, endTime, contentOnly, invertChecked });
+
     // If we have an empty profile (no samples), then don't invert the tree, as
     // it would hide the root node and a completely blank call tree space can be
     // mis-interpreted as an error.
     options.inverted = invertChecked && threadNode.samples > 0;
     this._populateCallTreeFromFrameNode(panel, threadNode, options);
   },
 
   /**
--- a/browser/devtools/profiler/utils/tree-model.js
+++ b/browser/devtools/profiler/utils/tree-model.js
@@ -39,75 +39,75 @@ exports.FrameNode.isContent = isContent;
  *       }
  *     }, // FrameNode
  *     ...
  *   }
  * } // ThreadNode
  *
  * @param object threadSamples
  *        The raw samples array received from the backend.
- * @param boolean contentOnly [optional]
- *        @see ThreadNode.prototype.insert
- * @param number beginAt [optional]
- *        @see ThreadNode.prototype.insert
- * @param number endAt [optional]
- *        @see ThreadNode.prototype.insert
- * @param boolean invert [optional]
- *        @see ThreadNode.prototype.insert
+ * @param object options
+ *        Additional supported options, @see ThreadNode.prototype.insert
+ *          - number startTime [optional]
+ *          - number endTime [optional]
+ *          - boolean contentOnly [optional]
+ *          - boolean invertTree [optional]
  */
-function ThreadNode(threadSamples, contentOnly, beginAt, endAt, invert) {
+function ThreadNode(threadSamples, options = {}) {
   this.samples = 0;
   this.duration = 0;
   this.calls = {};
   this._previousSampleTime = 0;
 
   for (let sample of threadSamples) {
-    this.insert(sample, contentOnly, beginAt, endAt, invert);
+    this.insert(sample, options);
   }
 }
 
 ThreadNode.prototype = {
   /**
    * Adds function calls in the tree from a sample's frames.
    *
    * @param object sample
    *        The { frames, time } sample, containing an array of frames and
    *        the time the sample was taken. This sample is assumed to be older
    *        than the most recently inserted one.
-   * @param boolean contentOnly [optional]
-   *        Specifies if platform frames shouldn't be taken into consideration.
-   * @param number beginAt [optional]
-   *        The earliest sample to start at (in milliseconds).
-   * @param number endAt [optional]
-   *        The latest sample to end at (in milliseconds).
-   * @param boolean inverted [optional]
-   *        Specifies if the call tree should be inverted (youngest -> oldest
-   *        frames).
+   * @param object options [optional]
+   *        Additional supported options:
+   *          - number startTime: the earliest sample to start at (in milliseconds)
+   *          - number endTime: the latest sample to end at (in milliseconds)
+   *          - boolean contentOnly: if platform frames shouldn't be used
+   *          - boolean invertTree: if the call tree should be inverted
    */
-  insert: function(sample, contentOnly = false, beginAt = 0, endAt = Infinity, inverted = false) {
+  insert: function(sample, options = {}) {
+    let startTime = options.startTime || 0;
+    let endTime = options.endTime || Infinity;
     let sampleTime = sample.time;
-    if (!sampleTime || sampleTime < beginAt || sampleTime > endAt) {
+    if (!sampleTime || sampleTime < startTime || sampleTime > endTime) {
       return;
     }
 
     let sampleFrames = sample.frames;
 
     // Filter out platform frames if only content-related function calls
     // should be taken into consideration.
-    if (contentOnly) {
+    if (options.contentOnly) {
       // The (root) node is not considered a content function, it'll be removed.
       sampleFrames = sampleFrames.filter(isContent);
     } else {
       // Remove the (root) node manually.
       sampleFrames = sampleFrames.slice(1);
     }
+    // If no frames remain after filtering, then this is a leaf node, no need
+    // to continue.
     if (!sampleFrames.length) {
       return;
     }
-    if (inverted) {
+    // Invert the tree after filtering, if preferred.
+    if (options.invertTree) {
       sampleFrames.reverse();
     }
 
     let sampleDuration = sampleTime - this._previousSampleTime;
     this._previousSampleTime = sampleTime;
     this.samples++;
     this.duration += sampleDuration;
 
--- a/browser/devtools/shared/options-view.js
+++ b/browser/devtools/shared/options-view.js
@@ -3,27 +3,26 @@ const { Services } = require("resource:/
 
 const OPTIONS_SHOWN_EVENT = "options-shown";
 const OPTIONS_HIDDEN_EVENT = "options-hidden";
 const PREF_CHANGE_EVENT = "pref-changed";
 
 /**
  * OptionsView constructor. Takes several options, all required:
  * - branchName: The name of the prefs branch, like "devtools.debugger."
- * - window: The window the XUL elements live in.
  * - menupopup: The XUL `menupopup` item that contains the pref buttons.
  *
  * Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as the second
  * argument. Fires events on opening/closing the XUL panel (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT)
  * as the second argument in the listener, used for tests mostly.
  */
 const OptionsView = function (options={}) {
   this.branchName = options.branchName;
-  this.window = options.window;
   this.menupopup = options.menupopup;
+  this.window = this.menupopup.ownerDocument.defaultView;
   let { document } = this.window;
   this.$ = document.querySelector.bind(document);
   this.$$ = document.querySelectorAll.bind(document);
 
   this.prefObserver = new PrefObserver(this.branchName);
 
   EventEmitter.decorate(this);
 };
@@ -74,16 +73,23 @@ OptionsView.prototype = {
   destroy: function () {
     this.mutationObserver.disconnect();
     this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
     this.menupopup.removeEventListener("popupshown", this._onPopupShown);
     this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
   },
 
   /**
+   * Returns the value for the specified `prefName`
+   */
+  getPref: function (prefName) {
+    return this.prefObserver.get(prefName);
+  },
+
+  /**
    * Called when a preference is changed (either via clicking an option
    * button or by changing it in about:config). Updates the checked status
    * of the corresponding button.
    */
   _onPrefChange: function (_, prefName) {
     let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
     let value = this.prefObserver.get(prefName);
 
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -20,16 +20,17 @@ support-files =
 [browser_flame-graph-02.js]
 [browser_flame-graph-03a.js]
 [browser_flame-graph-03b.js]
 [browser_flame-graph-04.js]
 [browser_flame-graph-utils-01.js]
 [browser_flame-graph-utils-02.js]
 [browser_flame-graph-utils-03.js]
 [browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-hash.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-hash.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+let test = Task.async(function*() {
+  let hash1 = FlameGraphUtils._getStringHash("abc");
+  let hash2 = FlameGraphUtils._getStringHash("acb");
+  let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
+  let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
+
+  isnot(hash1, hash2, "The hashes should not be equal (1).");
+  isnot(hash2, hash3, "The hashes should not be equal (2).");
+  isnot(hash3, hash4, "The hashes should not be equal (3).");
+
+  ok(Number.isInteger(hash1), "The hashes should be integers, not Infinity or NaN (1).");
+  ok(Number.isInteger(hash2), "The hashes should be integers, not Infinity or NaN (2).");
+  ok(Number.isInteger(hash3), "The hashes should be integers, not Infinity or NaN (3).");
+  ok(Number.isInteger(hash4), "The hashes should be integers, not Infinity or NaN (4).");
+
+  finish();
+});
--- a/browser/devtools/shared/test/browser_options-view-01.js
+++ b/browser/devtools/shared/test/browser_options-view-01.js
@@ -39,16 +39,20 @@ function* testOptionsView(tab) {
 
   // Test default config
   is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
   is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
 
   // Test buttons update when preferences update outside of the menu
   Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
   Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
+
+  is(options.getPref(PRETTY_PRINT_PREF), false, "getPref returns correct value");
+  is(options.getPref(BLACK_BOX_PREF), true, "getPref returns correct value");
+
   is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
   is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
 
   // Tests events are fired when preferences update outside of the menu
   is(events.length, 2, "two 'pref-changed' events fired");
   is(events[0], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
   is(events[1], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
 
@@ -60,16 +64,18 @@ function* testOptionsView(tab) {
   yield click(options, window, bbEl);
   is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
   is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF), false, "preference updated via click");
 
   // Tests events are fired when preferences updated via click
   is(events.length, 4, "two 'pref-changed' events fired");
   is(events[2], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
   is(events[3], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+  yield options.destroy();
 }
 
 function wait(window) {
   return new Promise(function (resolve, reject) {
   window.setTimeout(() => resolve, 60000);
   });
 }
 function createOptionsView (tab) {
--- a/browser/devtools/shared/widgets/FlameGraph.jsm
+++ b/browser/devtools/shared/widgets/FlameGraph.jsm
@@ -946,13 +946,17 @@ let FlameGraphUtils = {
     const STRING_HASH_PRIME1 = 7;
     const STRING_HASH_PRIME2 = 31;
 
     let hash = STRING_HASH_PRIME1;
 
     for (let i = 0, len = input.length; i < len; i++) {
       hash *= STRING_HASH_PRIME2;
       hash += input.charCodeAt(i);
+
+      if (hash > Number.MAX_SAFE_INTEGER / STRING_HASH_PRIME2) {
+        return hash;
+      }
     }
 
     return hash;
   }
 };
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -36,28 +36,33 @@
 <!-- LOCALIZATION NOTE (profilerUI.exportButton): This string is displayed
   -  on a button that opens a dialog to export a saved profile data file. -->
 <!ENTITY profilerUI.exportButton "Save">
 
 <!-- LOCALIZATION NOTE (profilerUI.clearButton): This string is displayed
   -  on a button that remvoes all the recordings. -->
 <!ENTITY profilerUI.clearButton "Clear">
 
-<!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
-  -  a checkbox that inverts and un-inverts the profiler's call tree. -->
-<!ENTITY profilerUI.invertTree "Invert Call Tree">
-
-<!-- LOCALIZATION NOTE (profilerUI.invertTree.tooltiptext): This is the tooltip
-  -  for the tree-inverting checkbox's label.  -->
-<!ENTITY profilerUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
-
 <!-- LOCALIZATION NOTE (profilerUI.table.*): These strings are displayed
   -  in the call tree headers for a recording. -->
 <!ENTITY profilerUI.table.totalDuration   "Total Time (ms)">
 <!ENTITY profilerUI.table.selfDuration    "Self Time (ms)">
 <!ENTITY profilerUI.table.totalPercentage "Total Cost">
 <!ENTITY profilerUI.table.selfPercentage  "Self Cost">
 <!ENTITY profilerUI.table.samples         "Samples">
 <!ENTITY profilerUI.table.function        "Function">
 
 <!-- LOCALIZATION NOTE (profilerUI.newtab.tooltiptext): The tooltiptext shown
   -  on the "+" (new tab) button for a profile when a selection is available. -->
 <!ENTITY profilerUI.newtab.tooltiptext "Add new tab from selection">
+
+<!-- LOCALIZATION NOTE (profilerUI.options.tooltiptext): This is the tooltip
+  -  for the options button. -->
+<!ENTITY profilerUI.options.tooltiptext "Configure performance preferences.">
+
+<!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
+  -  a checkbox that inverts and un-inverts the profiler's call tree. -->
+<!ENTITY profilerUI.invertTree "Invert Call Tree">
+
+<!-- LOCALIZATION NOTE (profilerUI.invertTree.tooltiptext): This is the tooltip
+  -  for the tree-inverting checkbox's label.  -->
+<!ENTITY profilerUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
+
index 9ee9cebcc1d260c22a5ed82f7892e3aa9738616b..cd2268577b9940c23e6af79d8395ead1b23def66
GIT binary patch
literal 832
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBgK_VAK!r32|kBfzZ%U2L}gZ<1`?{
z)6)}3#>U11$>`|l(9pD~sHo7y#F&`0h@_;rxQwXe<cNffsHCKrnB<tGq}bS$=*0Ax
zr1bdsl<0(%_~i81#FUtX^ti-~_@vaB<g~cN^mw3bVtQP1YHSivJS{dUBNikAWTeG`
z)Mvycr2#d@B>@@f5cP3DjUX~39!h2aRmLZS6aZC%1wraT3?K>D3r6uEkqn43B$NTw
z1rpBy>xGz^4lyJy8EQ^E#1N25h;>kMUb^aGU|^+`1o;IsFfuW-u(Gjpa`W)<3yMlg
zNy{rJsi<pe=^I<vIl21y1%$^XCMBn&W@KjPm(@15cXanpo;q#Lg5@h#t=qI^>$W|6
z_a8WT?8K=v=PzBoe(Uc2$IqU>eD(V6`;VW$e*6CO*Y7`n|E0Y@0E{=r7*7|+kcwMZ
zr$z=JG7xEX7G1sQO09FC;MUN>`=S#U>6W?*OwdxA_5Z*0oY{pv{?DHH?r%Sn@@$UD
zN!y*N|0kWe6lwY8Eyt?uTrm%#9{j5O*V<is_}Tp%6BM<1?H0XX>+QT^n)GSWb;%xa
zllksls^}AbvG8$quF%~xq6@Q4jd?GH%;hr+VBwmsHF3#K(Z!zAAKU(w>uovS`8biy
zu;d7*e&fmH*}jve2d(P3Sr@U}XSUzeC%ru@yEdF=)i>Gl{qZS|-7!UW3q>FPR$=~7
zzvjGvQ|5{nQmcH5y;<*;-@f%So#{{1>+C=`ht}QMorUuRPi5Hb>OS^DQ(bSF#F-L@
zzW&3Gd#38yOn6$kn0;Hx`tCzhF0<uq<hysW$kciAi>_ClLX#`~QjX6MZMa+gZCXnH
zZpJ%%t;4e;lqY}5-Ji5J$f$_PEZK8K_J>AY#{D`9zdl86d~y0v;zjX`KUUW;RTOVI
UQ{g;Q3zQ2yUHx3vIVCg!0Qmi5&j0`b
index f18e4cf1ebe026c8219a5018996d0bb88fc1bc59..2d90420bf31a68e333b66333807618ca6d945fc3
GIT binary patch
literal 606
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy4+DHcTp3^>G&Iz~!NJ%#4ao5H
z^mIr_h>eYnj*bouO^%C;i;PT(NKA~4O^HfM0+KOF>9I-aF$pQLiK(#(>2Zna@ktr6
zNg44V@wC{)jJV|VxTLiBL=Xw$0;Q7E;*!&V3gVK1vOtwUK_ELFB$AXK4`cuZAtaa^
z2VsC@GoZ@AA|S;{5c4yjBFPyL6Tr4WjD<3Q<i%@o9zgGTl?3?(GcYnSv#_$Ub8zzs
z35&}rDrxGQ*t+}rhb5#Hl$BRhR#i83PM$e?>GIWE_w3t$=*XF~=PzEpe&gn?+xMS5
zefIL*r_W!$e*5wB_uqeV7FW1|ratm?aSW+o+&Vea{IG+FL%gf@)+>vq8ExGn<|^5(
zB&Ni@^Z$Q#%X2kT%!_LZ_V1fB=gGd`Pxg28_^$2z@^gm#u~))u?Sib`hqXTYbt+G_
zy45UmURr9exW?D~7f~8*#p<Wxb$S~vE;@1LpYyldhu>^qxV`pz+1>u_aSwN157?SI
zf&Z3W?$yHM>)-l`1j$r-{0SC`klmhr@2R@z9KF>)V`jd*>ixrJhB*Ih&ON#2t}6Ft
z*IZE*f4}GO-(6>7R-Dw8sXcgeq5jUP#Rul!Ii{Rd)OSrqVC|c>P5Etn?oofGpMLpw
Z&fN5bi}A;($s0jw#M9N!Wt~$(697Qi0Sy2E
index 0895a217b4cb8c4548e2b258ac27322f9de34add..c6140a983e78a84ff6a3a7b285ca2e5c167ef307
GIT binary patch
literal 898
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz-ScU6XMDM1EHaz4h{~+#%Vx?
zr>CbwLPBh8ERY=?9UU5)7Lbq-8JQNEm>7|i6c?8km7E+Kn-Z0f9+Q+5nUDr#M<=Go
z$EQXoq{Sqq$HisDC#T0GWW*$-$0cUOB&WqCWyB<<#wDhMQ5sM%E;&68s1ih`#U^IN
zCZ@-N*cm_t@jx;O$W4n+N{4en?Bom}8Jh%B1{913aY5z)?ShgJDG(bZ3t`8>G{Q)b
zfglFdc9<ba5OctGLAg*iOb|-C$P2#$hF5AykY6wZBNH<VD;qlpHxD1bfQYD+oV=o{
zx`w8fwywUJor8x@Kwxl4OiFrwQF&E$b6b0NPj6rUgo%@;Oq;oI(ehPm)^6Imedpf&
zhmM~+f8pZQYu9hyz5nFd%U7@8fB5|6>-Qf&fBpXR_urodiXFh{n&s)@7*cWT?es{0
zCP#r5b{3Y9Cjz1_ADlig2nIOXIEqYl<`WdNOqu!rzj)8LV-7WiS)84m_b;bSe)Z;z
zU-<P6nv;)->U#t!zGHgY)^cY5L}50M`A_XrCUSAwxiCfSKhr+p=y%0)?j6Q~Ts19B
z!mf{$J(@n<tX;S2K-$b=QSXAwoDSv}C%u`>_PcjiLbJ+No{|YQszN1)((cav5m>g&
zvheYbthN&lYi5ci*$UZIOKy~@&HA%*?$$=Dldl^k*&dvoyhnVt`;)$Gza?{<+P;1h
z*lbw5!}-%Be#13+tK9f|7w{SDESkTiq0Ker?t(<Yr@B+yN<ZD_d0pN(?Ut&@)m{Ho
z?aD>+rhho;UZPz5<Be)u`PLg>^5y5&UAX*sSH-^y{dqSfqE8k)I(5#$EN;qq&Y2Qz
zb?ZG<SJm+zj<5+gQ+=MOko`R*u^@4&`0C?7mqmEpUZ=6QFeBxR1Z&W&j|*HEo$9k|
zUTTt^wZJFbVrga3j|z2@e_C<|9KkEjhR&K-Ymjz%vf9(z6<3ZPHk14Qk(<5l=|TP2
b;A5xMS!Av9xYZ`~g7T54tDnm{r-UW|XPkLR
new file mode 100644
index 0000000000000000000000000000000000000000..7ad7e2e0189456dc23dba61a529e043b7c64e45e
GIT binary patch
literal 1274
zc%17D@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wF4y>X9xI%xH7;%XlSUfZ(3|@ERf-s
zloTBu9gvU^6%`elm<SY%iAjq{N{Wlih>lK)NKA~2ON&ZQj!4LejZKM4N{Wd|j!sAg
z%El(9MJJ@hC8ftCrbfqS#3ZD|C#T0Irp6>>#3iN0CZ@zErNt$s#wVr5B&WqDr^Y9y
z$0el2BxS@UrpG6x$0er4CS`yGlT%}1+_=PyIG`aQjX<M-A|M1dHa!l^h)V+6n;xH>
z24aA@K$ReFVtOo)OwItAoSXqNFd1wMP&Nr<CRkYpG8d>9tQcZQ23!GHBa|BtQI?zz
zF*zO~n*n7cBdmyrs014eXT(7)MTo<=$p{;P<kPh}=YSE@Q4-`A%)rRR$;HjX%O@Zx
zDkdo<Eh8%@ub`-;Vrph?VQFP;YiIA^=;Y?<7Z4E{6&({7pOBc8oRXTJm7SYcP+U`2
z-`LdL(%RPE(b?TMd;Wrji<c~0zGCI7)oa$S+qh-xwxh?6pE!By^qI5g&R@KA{pQ^V
z4<9{#^7PsB7cbv``1tAbm#^Qx|M>aq_n*K2yfU0w85o$_JY5_^Dj471tPH-KAi(y)
z*CNX1mIxQusfP<g0|iBTS3FqQ5%y|r$Vv?jk+r87b!W+~y|qF9wxHzZtbd<xO3r-w
zo;fGo>BR3n=P&(VwCvcX;F&dclQ-#J66gxA3^tWh+A-sBld$pz#p$YL?HpRqKFQya
z`0~^GkGQ&vMd3F=b&1ud>{f{_yz%C=+7yxBUV>h2wW%Dc=l6YT5!`chmVE->vl*iD
z7aJ?5U8sMujdR+C^_dUST2(*T&$675%K3NYi<!ck?P}cW-z@4sy5dm5-G%uV&X+#^
zYV?28JPzTDH%sp{CVyGm;TimP%fkY((v-5jV$EXJytlSI_TwykVQw$-xLf(FipgC6
zW!L}dZZnC|?ELv%_mGjFPUpwV)tou!w@B=-xul@;dR~Q~cepgi>bxZ<=DDApAQ4?*
zU#aLMT_t|>hk5J)iQlC*PFL(ykFB3$w860Kt&fw<pHIDR6+KhuxjZxsoAtW!c!Xy4
zEtlgFFHc7>$R#gLnVr|Ocdcyl{Osd5`fnL-Tk)5<@0)Vf^1ASg`*P<7@6+G3wC3^C
z?d+lN-UJm*4|=VzugtOcp35oTSyE^Fr`%(Izi!UP7fYF{=WS!Uz5UXmCr8zkZdvO2
z{XLb@@S$vf!uhQIo)yQ%ZW!P8o#a)Pqqt{-&;Hbzb%|DSKcAEzl|JF4{_yJmy7n%C
zxF?L&UellI7R>&e^G|re?ssz%&tEERuDdmF#e!Ygi}PpN>RZ2PSGG(kzgys2##w&Y
z<uA{>b<ru(K{9qxN6W9@-K?Woa{bwXRc?x^tZv#;Q)gYVS{TIT_4{(v)@3<qQ+K~!
z(7k@z$EOzi?%n5Fct0#&D0BVJE8?ZMR=d4TUHNTScy;35r(y3*P46?hU%V<P*_j><
PD!)8k{an^LB{Ts50Q5Z7
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -145,10 +145,14 @@ skip-if = android_version == "10"
 [testJavascriptBridge]
 [testNativeCrypto]
 [testSessionHistory]
 
 # testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074
 [testSelectionHandler]
 skip-if = android_version == "10"
 
+# testTextareaSelections disabled on Android 2.3 by trailing skip-if, due to bug 980074
+[testTextareaSelections]
+skip-if = android_version == "10"
+
 [testStumblerSetting]
 skip-if = android_version == "10"
--- a/mobile/android/base/tests/roboextender/testSelectionHandler.html
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -22,16 +22,21 @@ Cu.import("resource://gre/modules/Servic
  */
 function startTests() {
   testSelectAllDivs().
     then(testSelectDivAtPoint).
     then(testSelectInput).
     then(testSelectTextarea).
     then(testReadonlyInput).
     then(testCloseSelection).
+    then(testStartSelectionFail).
+
+    then(testAttachCaret).
+    then(testAttachCaretFail).
+
     then(finishTests, function(err) {
       ok(false, "Error in selection test " + err);
       finishTests();
     });
 }
 
 /* =================================================================================
  *
@@ -46,20 +51,22 @@ function testSelectAllDivs() {
   // Check the initial state of the selection handler, and selectable/non-selectable <div>s.
   return Promise.all([
     ok(!sh.isSelectionActive(), "Selection should not be active at start of testSelectAllDivs"),
     ok(sh.canSelect(selDiv), "Can select selectable <div>"),
     ok(!sh.canSelect(nonSelDiv), "Can't select non-selectable <div>"),
 
   ]).then(function() {
     // Select all on a non-editable text node selects all the text in the page.
-    sh.startSelection(selDiv);
+    var startSelectionResult = sh.startSelection(selDiv);
     var selection = sh._getSelection();
 
     return Promise.all([
+      is(startSelectionResult, sh.ERROR_NONE,
+        "startSelection() should have completed successfully"),
       ok(sh.isSelectionActive(), "Selection should be active now"),
       is(selection.anchorNode, document.documentElement, "Anchor Node should be start of document"),
       is(selection.anchorOffset, 0, "Anchor offset should be 0"),
       is(selection.focusNode, document.body.lastChild, "Focus node should be lastChild of document"),
       is(selection.focusOffset, document.body.lastChild.textContent.length, "Focus offset should be it's length"),
     ]);
   });
 }
@@ -71,34 +78,36 @@ function testSelectAllDivs() {
  *
  */
 function testSelectDivAtPoint() {
   var sh = getSelectionHandler();
   var selDiv = document.getElementById("selDiv");
 
   // Select word at point in <div>
   var rect = selDiv.getBoundingClientRect();
-  sh.startSelection(selDiv, {
+  var startSelectionResult = sh.startSelection(selDiv, {
     mode: sh.SELECT_AT_POINT,
     x: rect.left + 1,
     y: rect.top + 1
   });
   var selection = sh._getSelection();
 
   // Check the state of the selection handler after selecting at a point.
   return Promise.all([
     ok(sh.isSelectionActive(), "Selection should be active at start of testSelectDivAtPoint"),
+    is(startSelectionResult, sh.ERROR_NONE,
+      "startSelection() should have completed successfully"),
     is(selection.toString(), DIV_POINT_TEXT, "The first word in the <div> was selected"),
 
   ]).then(function() {
     // Check the state of the selection handler after collapsing a selection.
     selection.collapseToStart();
 
     return Promise.all([
-      ok(selection.getRangeAt(0).collapsed, "Selection should be collapsed"),
+      ok(selection.collapsed, "Selection should be collapsed"),
       ok(!sh.isSelectionActive(), "Selection should not be active"),
     ]);
   });
 }
 
 /* =================================================================================
  *
  * "Select all" text selection test, for <input> (editable) field.
@@ -111,20 +120,22 @@ function testSelectInput() {
 
   // Test that calling startSelection with an input selects all the text in the input.
   return Promise.all([
     ok(!sh.isSelectionActive(), "Selection should not be active at start of testSelectInput"),
     ok(sh.canSelect(inputNode), "Can select selectable <input>"),
 
   ]).then(function() {
     // Check the state of the selection handler after calling startSelection on it.
-    sh.startSelection(inputNode);
+    var startSelectionResult = sh.startSelection(inputNode);
     var selection = sh._getSelection();
 
     return Promise.all([
+      is(startSelectionResult, sh.ERROR_NONE,
+        "startSelection() should have completed successfully"),
       ok(sh.isSelectionActive(), "Selection should be active"),
       ok((sh._targetElement instanceof Ci.nsIDOMNSEditableElement), "Selected element is editable"),
       is(selection.toString(), INPUT_TEXT, "All text in the <input> was selected"),
     ]);
   });
 }
 
 /* =================================================================================
@@ -134,21 +145,23 @@ function testSelectInput() {
  */
 
 function testSelectTextarea() {
   var sh = getSelectionHandler();
   var textareaNode = document.getElementById("textareaNode");
   textareaNode.value = TEXTAREA_TEXT;
 
   // Change (still-active) selection from previous <input> field to <textarea>
-  sh.startSelection(textareaNode);
+  var startSelectionResult = sh.startSelection(textareaNode);
   var selection = sh._getSelection();
 
   return Promise.all([
     ok(sh.isSelectionActive(), "Selection should be active at start of testSelectTextarea"),
+    is(startSelectionResult, sh.ERROR_NONE,
+      "startSelection() should have completed successfully"),
     ok((sh._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement), "Selected element is editable, and a <textarea>"),
     is(selection.toString(), TEXTAREA_TEXT, "All text in the <textarea> was selected"),
 
   ]).then(function() {
     // Collpase the selection to close it again.
     selection.collapseToStart();
 
     return Promise.all([
@@ -217,16 +230,131 @@ function testCloseSelection() {
     sh.startSelection(inputNode);
     sh.handleEvent({ type: "blur" });
     return ok(!sh.isSelectionActive(), "blur should close active selection");
   });
 }
 
 /* =================================================================================
  *
+ * Various text selection tests to ensure we fail certain startSelection() requests.
+ *
+ */
+function testStartSelectionFail() {
+  var sh = getSelectionHandler();
+
+  return Promise.all([
+    ok(!sh.isSelectionActive(),
+      "Selection should not be active at start of testStartSelectionFail"),
+
+  ]).then(function() {
+    // We cannot perform an invalid selection request.
+    var element = document.getElementById("inputNode");
+    var rect = element.getBoundingClientRect();
+    var startSelectionResult = sh.startSelection(element, {
+      mode: "fooMode",
+      x: rect.left + 1,
+      y: rect.top + 1
+    });
+
+    return Promise.all([
+      is(startSelectionResult, sh.START_ERROR_INVALID_MODE,
+        "startSelection() should have failed predictably."),
+      ok(!sh.isSelectionActive(), "We cannot select text with a bad mode request."),
+    ]);
+
+  }).then(function() {
+    // Select all on a Button should fail.
+    var element = document.getElementById("inputButton");
+    var startSelectionResult = sh.startSelection(element);
+
+    return Promise.all([
+      is(startSelectionResult, sh.START_ERROR_NONTEXT_INPUT,
+        "startSelection() should have failed predictably."),
+      ok(!sh.isSelectionActive(), "We cannot select text in an input Button."),
+    ]);
+
+  }).then(function() {
+    // We cannot Select Word where no point exists.
+    var element = document.getElementById("inputNode");
+    var rect = element.getBoundingClientRect();
+    var startSelectionResult = sh.startSelection(element, {
+      mode: sh.SELECT_AT_POINT,
+      x: -1000,
+      y: -1000
+    });
+
+    return Promise.all([
+      is(startSelectionResult, sh.START_ERROR_SELECT_WORD_FAILED,
+        "startSelection() should have failed predictably."),
+      ok(!sh.isSelectionActive(), "We cannot select text at a bad location request."),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * Test to ensure we can attach a Caret to an input field.
+ *
+ */
+function testAttachCaret() {
+  var sh = getSelectionHandler();
+
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testAttachCaret"),
+
+  ]).then(function() {
+    var element = document.getElementById("inputNode");
+    element.value = INPUT_TEXT;
+    var attachCaretResult = sh.attachCaret(element);
+
+    return Promise.all([
+      is(attachCaretResult, sh.ERROR_NONE,
+        "attachCaret() should have completed successfully"),
+    ]);
+
+  }).then(function() {
+    sh.observe(null, "TextSelection:End", {});
+
+    return Promise.all([
+      ok(!sh.isSelectionActive(), "Selection should not be active at end of testAttachCaret"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * Test to ensure we fail certain attachCaret() requests.
+ *
+ */
+function testAttachCaretFail() {
+  var sh = getSelectionHandler();
+
+  return Promise.all([
+    is(sh._activeType, sh.TYPE_NONE,
+      "Selection should not be active at start of testAttachCaretFail."),
+
+  ]).then(function() {
+    // We cannot attach Caret into disabled input.
+    var element = document.getElementById("inputDisabled");
+    element.value = INPUT_TEXT;
+    var attachCaretResult = sh.attachCaret(element);
+
+    return Promise.all([
+      is(attachCaretResult, sh.ATTACH_ERROR_INCOMPATIBLE,
+        "attachCaret() should have failed predictably."),
+      is(sh._activeType, sh.TYPE_NONE,
+        "Selection should not be active at end of testAttachCaretFail."),
+    ]);
+  });
+}
+
+
+/* =================================================================================
+ *
  * After finish of all selection tests, wrap up and go home.
  *
  */
 function finishTests() {
   Messaging.sendRequest({
     type: "Robocop:testSelectionHandler",
     result: true,
     msg: "Done!",
@@ -297,10 +425,13 @@ function is(one, two, msg) {
       ligula interdum enim, vel varius libero sem ut ligula.</div><br>
 
     <input id="inputNode" type="text"><br>
 
     <textarea id="textareaNode"></textarea><br>
 
     <input id="readOnlyTextInput" type="text" readonly><br>
 
+    <input id="inputButton" type="button" value="Click me"><br>
+
+    <input id="inputDisabled" type="text" disabled><br>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/roboextender/testTextareaSelections.html
@@ -0,0 +1,854 @@
+<html>
+  <head>
+    <title>Automated RTL/LTR Text Selection tests for Textareas</title>
+    <meta name="viewport" content="initial-scale=1.0"/>
+    <script type="application/javascript"
+      src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript">
+
+// Used to create handle movement events for SelectionHandler.
+const ANCHOR = "ANCHOR";
+const FOCUS = "FOCUS";
+
+// Used to specifiy midpoint selection text left/right of center.
+const EST_SEL_TEXT_BOUND_CHARS = 5;
+
+// Used to ensure calculated coords for handle movement events get us
+// "into" the next/prev line vertically.
+const EST_SEL_LINE_CHG_PTS = 10;
+
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/Geometry.jsm');
+
+// Distance between text selection lines. Reality tested, and also
+// Used to perform multi-line selection selections.
+var selectionLineHeight = 0;
+
+/* =================================================================================
+ *
+ * Start of all text selection tests, check initialization state.
+ */
+function startTests() {
+  testLTR_selectionPoints().
+    then(testRTL_selectionPoints).
+
+    then(test_selectionLineHeight).
+
+    then(testLTR_moveFocusHandleDown).
+    then(testLTR_moveFocusHandleUp).
+    then(testLTR_moveAnchorHandleUp).
+    then(testLTR_moveAnchorHandleDown).
+
+    then(testRTL_moveFocusHandleDown).
+    then(testRTL_moveFocusHandleUp).
+    then(testRTL_moveAnchorHandleUp).
+    then(testRTL_moveAnchorHandleDown).
+
+    then(finishTests, function(err) {
+      ok(false, "Error in selection test " + err);
+      finishTests();
+    });
+}
+
+/* =================================================================================
+ *
+ * LTR Textarea test will create a single line selection in the middle of the element
+ * and ensure that the anchor point is to the left of the focus point.
+ */
+function testLTR_selectionPoints() {
+  // Select entire LTRTextArea.
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  sh.startSelection(element);
+
+  return Promise.all([
+    ok(sh.isSelectionActive(),
+      "testLTR_selectionPoints starts, selection should be active."),
+
+  ]).then(function() {
+    // setSelectionRange() (in editable elements), gets us a single-line selection of
+    // midpoint character +- EST_SEL_TEXT_BOUND_CHARS chars on either side.
+    var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+    element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                              midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+    // Grab values that are cleared by closing selection.
+    var selection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                      focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+    var midpointSelText = sh._getSelectedText();
+
+    // Close selection and complete test.
+    sh.observe(null, "TextSelection:End", {});
+
+    return Promise.all([
+      selectionExists(selection, "LTR Selection existed at points"),
+
+      is(midpointSelText, " plasma of", "LTR Selection should match expected text"),
+      is(selection.anchorPt.y, selection.focusPt.y,
+        "LTR Selection anchorPt should match focusPt vertically"),
+      lessThan(selection.anchorPt.x, selection.focusPt.x,
+        "LTR Selection anchorPt should be the left of focusPt"),
+      ok(!sh.isSelectionActive(),
+        "testLTR_selectionPoints finishes, selection should not be active."),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * RTL Textarea test will create a single line selection in the middle of the element
+ * and ensure that the anchor point is to the right of the focus point.
+ */
+function testRTL_selectionPoints() {
+  // Select entire RTLTextArea.
+  var sh = getSelectionHandler();
+  var element = document.getElementById("RTLTextarea");
+  sh.startSelection(element);
+
+  return Promise.all([
+    ok(sh.isSelectionActive(),
+      "testRTL_selectionPoints starts, selection should be active."),
+
+  ]).then(function() {
+    // setSelectionRange() (in editable elements), gets us a single-line selection of
+    // midpoint character +- EST_SEL_TEXT_BOUND_CHARS chars on either side.
+    var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+    element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                              midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+    // Grab values that are cleared by closing selection.
+    var selection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                      focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+    var midpointSelText = sh._getSelectedText();
+
+    // Close selection and complete test.
+    sh.observe(null, "TextSelection:End", {});
+
+    return Promise.all([
+      selectionExists(selection, "RTL Selection existed at points"),
+
+      is(midpointSelText, "ל גם את הב", "RTL Selection should match expected text"),
+      is(selection.anchorPt.y, selection.focusPt.y,
+        "RTL Selection anchorPt should match focusPt vertically"),
+      greaterThan(selection.anchorPt.x, selection.focusPt.x,
+        "RTL Selection anchorPt should be to the right of focusPt"),
+      ok(!sh.isSelectionActive(),
+        "testRTL_selectionPoints finishes, selection should not be active."),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * Textarea test will create (a) a single-line selection in the middle of the element,
+ * move the focus handle down a line creating (b) a two-line selection, and then
+ * ensure that the vertical distance between the bottom of (a) and (b) is > 0.
+ *
+ * The result is used later to ensure more-precise handle up/down movements.
+ */
+function test_selectionLineHeight() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection focus to next lower line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : FOCUS,
+      x : initialSelection.focusPt.x,
+      y : initialSelection.focusPt.y + EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : FOCUS })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Note selection line height for reality test,
+  // and later handle movement calculations.
+  selectionLineHeight = changedSelection.focusPt.y - initialSelection.focusPt.y;
+
+  return Promise.all([
+    ok(sh.isSelectionActive(),
+      "test_selectionLineHeight starts, selection should be active."),
+
+  ]).then(function() {
+    // Complete test, and report.
+    sh.observe(null, "TextSelection:End", {});
+
+    return Promise.all([
+      greaterThan(selectionLineHeight, 0, "Distance from one line to another " +
+        "in a multi-line selection is greater than 0."),
+
+      ok(!sh.isSelectionActive(),
+        "test_selectionLineHeight finishes, selection should not be active."),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * LTR Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during focus handle down movements.
+ */
+function testLTR_moveFocusHandleDown() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection focus to next lower line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : FOCUS,
+      x : initialSelection.focusPt.x,
+      y : initialSelection.focusPt.y + EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : FOCUS })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testLTR_moveFocusHandleDown - Test Starts."),
+
+    selectionExists(initialSelection, "LTR Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "LTR Initial selection anchorPt.y should match focusPt.y"),
+    lessThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "LTR Initial selection anchorPt.x should be less than (left of) focusPt.x"),
+
+    selectionExists(changedSelection, "LTR Changed selection existed at points"),
+    pointEquals(changedSelection.anchorPt, initialSelection.anchorPt,
+      "LTR Changed selection focus handle moving down " +
+      "should not change anchor handle."),
+    greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+      "LTR Changed selection focusPt.y " +
+      "should be greater than (below) changed anchorPt.y"),
+
+    greaterThan(changedSelection.focusPt.y, initialSelection.focusPt.y,
+      "LTR Changed selection focusPt.y " +
+      "should be greater than (below) Initial selection focusPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * LTR Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during focus handle up movements.
+ */
+
+function testLTR_moveFocusHandleUp() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection focus to next upper line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : FOCUS,
+      x : initialSelection.focusPt.x,
+      y : initialSelection.focusPt.y - selectionLineHeight - EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : FOCUS })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testLTR_moveFocusHandleUp - Test Starts."),
+
+    selectionExists(initialSelection, "LTR Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "LTR Initial selection anchorPt.y should match focusPt.y"),
+    lessThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "LTR Initial selection anchorPt.x should be less than (left of) focusPt.x"),
+
+    selectionExists(changedSelection, "LTR Changed selection existed at points"),
+    pointEquals(changedSelection.focusPt, initialSelection.anchorPt,
+      "LTR Reversed Changed selection focus handle moving up " +
+      "becomes new anchor handle, " +
+      "new focus handle is initial anchor handle."),
+    greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+      "LTR Reversed Changed selection focusPt.y " +
+      "should be greater than (below) changed anchorPt.y"),
+
+    is(changedSelection.focusPt.y, initialSelection.focusPt.y,
+      "LTR Reversed Changed selection focusPt.y " +
+      "should be equal-to Initial selection focusPt.y"),
+    lessThan(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+      "LTR Reversed Changed selection anchorPt.y " +
+      "should be less than (above) Initial selection anchorPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * LTR Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during anchor handle up movements.
+ */
+function testLTR_moveAnchorHandleUp() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection anchor to next upper line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : ANCHOR,
+      x : initialSelection.anchorPt.x,
+      y : initialSelection.anchorPt.y - selectionLineHeight - EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : ANCHOR })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testLTR_moveAnchorHandleUp - Test Starts."),
+
+    selectionExists(initialSelection, "LTR Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "LTR Initial selection anchorPt.y should match focusPt.y"),
+    lessThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "LTR Initial selection anchorPt.x should be less than (left of) focusPt.x"),
+
+    selectionExists(changedSelection, "LTR Changed selection existed at points"),
+    pointEquals(changedSelection.focusPt, initialSelection.focusPt,
+      "LTR Changed selection anchor handle moving up " +
+      "should not change focus handle."),
+    greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+      "LTR Changed selection focusPt.y " +
+      "should be greater than (below) changed anchorPt.y"),
+
+    lessThan(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+      "LTR Changed selection anchorPt.y " +
+      "should be less than (above) Initial selection anchorPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * LTR Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during anchor handle down movements.
+ */
+function testLTR_moveAnchorHandleDown() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("LTRTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection anchor to next lower line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : ANCHOR,
+      x : initialSelection.anchorPt.x,
+      y : initialSelection.anchorPt.y + EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : ANCHOR })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testLTR_moveAnchorHandleDown - Test Starts."),
+
+    selectionExists(initialSelection, "LTR Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "LTR Initial selection anchorPt.y should match focusPt.y"),
+    lessThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "LTR Initial selection anchorPt.x should be less than (left of) focusPt.x"),
+
+    selectionExists(changedSelection, "LTR Changed selection existed at points"),
+    pointEquals(changedSelection.anchorPt, initialSelection.focusPt,
+      "LTR Reversed Changed selection anchor handle moving down " +
+      "becomes new focus handle, " +
+      "new anchor handle is initial focus handle."),
+    greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+      "LTR Reversed Changed selection focusPt.y " +
+      "should be greater than (below) changed anchorPt.y"),
+
+    is(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+      "LTR Reversed Changed selection anchorPt.y " +
+      "should be equal to Initial selection anchorPt.y"),
+    greaterThan(changedSelection.focusPt.y, initialSelection.focusPt.y,
+      "LTR Reversed Changed selection focusPt.y " +
+      "should be greater than (below) Initial selection focusPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * RTL Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during focus handle down movements.
+ */
+function testRTL_moveFocusHandleDown() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("RTLTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection focus to next lower line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : FOCUS,
+      x : initialSelection.focusPt.x,
+      y : initialSelection.focusPt.y + EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : FOCUS })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testRTL_moveFocusHandleDown - Test Starts."),
+
+    selectionExists(initialSelection, "RTL Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "RTL Initial selection anchorPt.y should match focusPt.y"),
+    greaterThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "RTL Initial selection anchorPt.x should be greater than (right of) focusPt.x"),
+
+    selectionExists(changedSelection, "RTL Changed selection existed at points"),
+    todo(false, "testRTL_moveFocusHandleDown: " +
+    // pointEquals(changedSelection.anchorPt, initialSelection.anchorPt,
+       "RTL Changed selection focus handle moving down " +
+       "should not change anchor handle position."),
+    todo(false, "testRTL_moveFocusHandleDown: " +
+    // greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+       "RTL Changed selection focusPt.y " +
+       "should be greater than (below) changed anchorPt.y"),
+
+    todo(false, "testRTL_moveFocusHandleDown: " +
+    // greaterThan(changedSelection.focusPt.y, initialSelection.focusPt.y,
+       "RTL Changed selection focusPt.y " +
+       "should be greater than (below) Initial selection focusPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * RTL Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during focus handle up movements.
+ */
+function testRTL_moveFocusHandleUp() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("RTLTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection focus to next upper line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : FOCUS,
+      x : initialSelection.focusPt.x,
+      y : initialSelection.focusPt.y - selectionLineHeight - EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : FOCUS })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testRTL_moveFocusHandleUp - Test Starts."),
+
+    selectionExists(initialSelection, "RTL Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "RTL Initial selection anchorPt.y should match focusPt.y"),
+    greaterThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "RTL Initial selection anchorPt.x should be greater than (right of) focusPt.x"),
+
+    selectionExists(changedSelection, "RTL Changed selection existed at points"),
+    todo(false, "testRTL_moveFocusHandleUp: " +
+    // pointEquals(changedSelection.focusPt, initialSelection.anchorPt,
+       "RTL Reversed Changed selection focus handle moving up " +
+       "becomes new anchor handle, " +
+       "new focus handle is initial anchor handle."),
+    todo(false, "testRTL_moveFocusHandleUp: " +
+    // greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+       "RTL Reversed Changed selection focusPt.y " +
+       "should be greater than (below) changed anchorPt.y"),
+
+    todo(false, "testRTL_moveFocusHandleUp: " +
+    // is(changedSelection.focusPt.y, initialSelection.focusPt.y,
+       "RTL Reversed Changed selection focusPt.y " +
+       "should be equal to Initial selection focusPt.y"),
+    todo(false, "testRTL_moveFocusHandleUp: " +
+    // lessThan(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+       "RTL Reversed Changed selection anchorPt.y " +
+       "should be less than (above) Initial selection anchorPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * RTL Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during anchor handle up movements.
+ */
+function testRTL_moveAnchorHandleUp() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("RTLTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection anchor to next upper line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : ANCHOR,
+      x : initialSelection.anchorPt.x,
+      y : initialSelection.anchorPt.y - selectionLineHeight - EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : ANCHOR })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testRTL_moveAnchorHandleUp - Test Starts."),
+
+    selectionExists(initialSelection, "RTL Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "RTL Initial selection anchorPt.y should match focusPt.y"),
+    greaterThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "RTL Initial selection anchorPt.x should be greater than (right of) focusPt.x"),
+
+    selectionExists(changedSelection, "RTL Changed selection existed at points"),
+    todo(false, "testRTL_moveAnchorHandleUp: " +
+    // pointEquals(changedSelection.focusPt, initialSelection.focusPt,
+       "RTL Changed selection anchor handle moving up " +
+       "should not change focus handle position."),
+    todo(false, "testRTL_moveAnchorHandleUp: " +
+    // greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+       "RTL Changed selection focusPt.y " +
+       "should be greater than (below) changed anchorPt.y"),
+
+    todo(false, "testRTL_moveAnchorHandleUp: " +
+    // lessThan(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+       "RTL Changed selection anchorPt.y " +
+       "should be less than (above) Initial selection anchorPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * RTL Textarea test will create a single-line selection in the middle of the element
+ * and ensure that handle reversals are detected as expected.
+ *
+ * This tests what happens during anchor handle down movements.
+ */
+function testRTL_moveAnchorHandleDown() {
+  var sh = getSelectionHandler();
+  var element = document.getElementById("RTLTextarea");
+  var initialSelection = null;
+  var changedSelection = null;
+
+  // Select entire textarea, refine selection to midpoint string.
+  sh.startSelection(element);
+  var midpointSelCharOffset = (element.selectionStart + element.selectionEnd) / 2;
+  element.setSelectionRange(midpointSelCharOffset - EST_SEL_TEXT_BOUND_CHARS,
+                            midpointSelCharOffset + EST_SEL_TEXT_BOUND_CHARS);
+
+  // Note initial selection points.
+  initialSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Force selection anchor to next lower line (estimate distance required).
+  sh.observe(null, "TextSelection:Move",
+    JSON.stringify({ handleType : ANCHOR,
+      x : initialSelection.anchorPt.x,
+      y : initialSelection.anchorPt.y + EST_SEL_LINE_CHG_PTS
+    })
+  );
+  sh.observe(null, "TextSelection:Position",
+    JSON.stringify({ handleType : ANCHOR })
+  );
+
+  // Note changed selection points after handle movement.
+  changedSelection = { anchorPt : new Point(sh._cache.anchorPt.x, sh._cache.anchorPt.y),
+                       focusPt : new Point(sh._cache.focusPt.x, sh._cache.focusPt.y) };
+
+  // Complete test, and report.
+  sh.observe(null, "TextSelection:End", {});
+
+  return Promise.all([
+    ok(true, "testRTL_moveAnchorHandleDown - Test Starts."),
+
+    selectionExists(initialSelection, "RTL Initial selection existed at points"),
+    is(initialSelection.anchorPt.y, initialSelection.focusPt.y,
+      "RTL Initial selection anchorPt.y should match focusPt.y"),
+    greaterThan(initialSelection.anchorPt.x, initialSelection.focusPt.x,
+      "RTL Initial selection anchorPt.x should be greater than (right of) focusPt.x"),
+
+    selectionExists(changedSelection, "RTL Changed selection existed at points"),
+    todo(false, "testRTL_moveAnchorHandleDown: " +
+    // pointEquals(changedSelection.anchorPt, initialSelection.focusPt,
+       "RTL Reversed Changed selection anchor handle moving down " +
+       "becomes new focus handle, " +
+       "new anchor handle is initial focus handle."),
+    todo(false, "testRTL_moveAnchorHandleDown: " +
+    // greaterThan(changedSelection.focusPt.y, changedSelection.anchorPt.y,
+       "RTL Reversed Changed selection focusPt.y " +
+       "should be greater than (below) changed anchorPt.y"),
+
+    todo(false, "testRTL_moveAnchorHandleDown: " +
+    // is(changedSelection.anchorPt.y, initialSelection.anchorPt.y,
+       "RTL Reversed Changed selection anchorPt.y " +
+       "should be equal to Initial selection anchorPt.y"),
+    todo(false, "testRTL_moveAnchorHandleDown: " +
+    // greaterThan(changedSelection.focusPt.y, initialSelection.focusPt.y,
+       "RTL Reversed Changed selection focusPt.y " +
+       "should be greater than (below) Initial selection focusPt.y"),
+  ]);
+}
+
+/* =================================================================================
+ *
+ * After finish of all selection tests, wrap up and go home.
+ *
+ */
+function finishTests() {
+  Messaging.sendRequest({
+    type: "Robocop:testTextareaSelections",
+    result: true,
+    msg: "Done!",
+    done: true
+  });
+}
+
+/* ============================== Utility functions ======================
+ *
+ * Common functions available to all tests.
+ *
+ */
+function getSelectionHandler() {
+  return (!this._selectionHandler) ?
+    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
+    this._selectionHandler;
+}
+
+function todo(result, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    todo: result,
+    msg: msg
+  });
+}
+
+function ok(result, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: result,
+    msg: msg
+  });
+}
+
+function is(one, two, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: one === two,
+    msg: msg + " : " + one + " === " + two
+  });
+}
+
+function lessThan(n1, n2, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: n1 < n2,
+    msg: msg + " : " + n1 + " < " + n2
+  });
+}
+
+function greaterThan(n1, n2, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: n1 > n2,
+    msg: msg + " : " + n1 + " > " + n2
+  });
+}
+
+function pointEquals(p1, p2, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: p1.equals(p2),
+    msg: msg + " : " + p1.toString() + " == " + p2.toString()
+  });
+}
+
+function pointNotEquals(p1, p2, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: !p1.equals(p2),
+    msg: msg + " : " + p1.toString() + " == " + p2.toString()
+  });
+}
+
+function selectionExists(selection, msg) {
+  return Messaging.sendRequestForResult({
+    type: "Robocop:testTextareaSelections",
+    result: !selection.anchorPt.equals(selection.focusPt),
+    msg: msg + " : anchor:" + selection.anchorPt.toString() +
+      " focus:" + selection.focusPt.toString()
+  });
+}
+
+/* =================================================================================
+ *
+ * Page definition for all tests.
+ *
+ */
+    </script>
+  </head>
+
+  <body onload="startTests();">
+    <textarea id="LTRTextarea" style="direction: ltr;" rows="10" cols="40"
+      readonly="true">Under sufficiently extreme conditions, quarks may become deconfined and exist as free particles. In the course of asymptotic freedom, the strong interaction becomes weaker at higher temperatures. Eventually, color confinement would be lost and an extremely hot plasma of freely moving quarks and gluons would be formed. This theoretical phase of matter is called quark-gluon plasma.[81] The exact conditions needed to give rise to this state are unknown and have been the subject of a great deal of speculation and experimentation.</textarea>
+
+    <textarea id="RTLTextarea" style="direction: rtl;" rows="10" cols="40"
+      readonly="true">טטיאנה קוזמינה, שהייתה 18, תלמיד תיכון בעפולה, עלה לישראל לפני כשנים עם האמא שלה, שהיה נשואה לאזרח ישראלי, כאשר אביה הביולוגי חתם על מסמך המאשר את המהלך שלה לישראל. האמא שלה היא בתהליך של התאזרחות חשב שזה כולל גם את הבת שלה, אבל ברגע שהיא הבינה כבר לפני כמה חודשים שהחברה המאוחדת לא נכללה בו, דחה את הבקשה לעבד גם לבת שלה. ואז הם קיבלו את הגור.וחד-קרן הגיעה, אבל הם לא הצליחו למצוא את קשת אז כולם אכלו ספגטי וכנפיים בופל חמים.</textarea>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testTextareaSelections.java
@@ -0,0 +1,50 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+
+public class testTextareaSelections extends UITest {
+
+    public void testTextareaSelections() {
+        GeckoHelper.blockForReady();
+
+        Actions.EventExpecter robocopTestExpecter =
+            getActions().expectGeckoEvent("Robocop:testTextareaSelections");
+        final String url = "chrome://roboextender/content/testTextareaSelections.html";
+        NavigationHelper.enterAndLoadUrl(url);
+        mToolbar.assertTitle(url);
+
+        while (!test(robocopTestExpecter)) {
+            // do nothing
+        }
+
+        robocopTestExpecter.unregisterListener();
+    }
+
+    private boolean test(Actions.EventExpecter expecter) {
+        final JSONObject eventData;
+        try {
+            eventData = new JSONObject(expecter.blockForEventData());
+        } catch(Exception ex) {
+            // Log and ignore
+            getAsserter().ok(false, "JS Test", "Error decoding data " + ex);
+            return false;
+        }
+
+        if (eventData.has("result")) {
+            getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg"));
+        } else if (eventData.has("todo")) {
+            getAsserter().todo(eventData.optBoolean("todo"), "JS TODO", eventData.optString("msg"));
+        }
+
+        EventDispatcher.sendResponse(eventData, new JSONObject());
+        return eventData.optBoolean("done", false);
+    }
+}
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java
@@ -198,20 +198,16 @@ public class ToolbarDisplayLayout extend
 
     @Override
     public void onAttachedToWindow() {
         mIsAttached = true;
 
         Button.OnClickListener faviconListener = new Button.OnClickListener() {
             @Override
             public void onClick(View view) {
-                if (mSiteSecurity.getVisibility() != View.VISIBLE) {
-                    return;
-                }
-
                 mSiteIdentityPopup.show();
             }
         };
 
         mFavicon.setOnClickListener(faviconListener);
         mSiteSecurity.setOnClickListener(faviconListener);
 
         mStop.setOnClickListener(new Button.OnClickListener() {
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -1,18 +1,35 @@
 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // Define elements that bound phone number containers.
 const PHONE_NUMBER_CONTAINERS = "td,div";
+const DEFER_CLOSE_TRIGGER_MS = 125; // Grace period delay before deferred _closeSelection()
 
 var SelectionHandler = {
+
+  // Successful startSelection() or attachCaret().
+  ERROR_NONE: "",
+
+  // Error codes returned during startSelection().
+  START_ERROR_INVALID_MODE: "Invalid selection mode requested.",
+  START_ERROR_NONTEXT_INPUT: "Target element by definition contains no text.",
+  START_ERROR_NO_WORD_SELECTED: "No word selected at point.",
+  START_ERROR_SELECT_WORD_FAILED: "Word selection at point failed.",
+  START_ERROR_SELECT_ALL_PARAGRAPH_FAILED: "Select-All Paragraph failed.",
+  START_ERROR_NO_SELECTION: "Selection performed, but nothing resulted.",
+  START_ERROR_PROXIMITY: "Selection target and result seem unrelated.",
+
+  // Error codes returned during attachCaret().
+  ATTACH_ERROR_INCOMPATIBLE: "Element disabled, handled natively, or not editable.",
+
   HANDLE_TYPE_ANCHOR: "ANCHOR",
   HANDLE_TYPE_CARET: "CARET",
   HANDLE_TYPE_FOCUS: "FOCUS",
 
   TYPE_NONE: 0,
   TYPE_CURSOR: 1,
   TYPE_SELECTION: 2,
 
@@ -21,16 +38,17 @@ var SelectionHandler = {
 
   // Keeps track of data about the dimensions of the selection. Coordinates
   // stored here are relative to the _contentWindow window.
   _cache: null,
   _activeType: 0, // TYPE_NONE
   _draggingHandles: false, // True while user drags text selection handles
   _ignoreCompositionChanges: false, // Persist caret during IME composition updates
   _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
+  _deferCloseTimer: null, // Used to defer _closeSelection() actions during programmatic changes
 
   // TargetElement changes (text <--> no text) trigger actionbar UI update
   _prevTargetElementHasText: null,
 
   // The window that holds the selection (can be a sub-frame)
   get _contentWindow() {
     if (this._contentWindowRef)
       return this._contentWindowRef.get();
@@ -84,16 +102,21 @@ var SelectionHandler = {
     Services.obs.removeObserver(this, "TextSelection:LayerReflow");
 
     BrowserApp.deck.removeEventListener("pagehide", this, false);
     BrowserApp.deck.removeEventListener("blur", this, true);
     BrowserApp.deck.removeEventListener("scroll", this, true);
   },
 
   observe: function sh_observe(aSubject, aTopic, aData) {
+    // Ignore all but selectionListener notifications during deferred _closeSelection().
+    if (this._deferCloseTimer) {
+      return;
+    }
+
     switch (aTopic) {
       // Update handle/caret position on page reflow (keyboard open/close,
       // dynamic DOM changes, orientation updates, etc).
       case "TextSelection:LayerReflow": {
         if (this._activeType == this.TYPE_SELECTION) {
           this._updateCacheForSelection();
         }
         if (this._activeType != this.TYPE_NONE) {
@@ -211,16 +234,21 @@ var SelectionHandler = {
   _stopDraggingHandles: function sh_stopDraggingHandles() {
     if (this._draggingHandles) {
       this._draggingHandles = false;
       Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: false });
     }
   },
 
   handleEvent: function sh_handleEvent(aEvent) {
+    // Ignore all but selectionListener notifications during deferred _closeSelection().
+    if (this._deferCloseTimer) {
+      return;
+    }
+
     switch (aEvent.type) {
       case "scroll":
         // Maintain position when top-level document is scrolled
         this._positionHandlesOnChange();
         break;
 
       case "pagehide": {
         // We only care about events on the selected tab.
@@ -263,33 +291,44 @@ var SelectionHandler = {
     this._contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor).
                             getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
     return {
       X: scrollX.value,
       Y: scrollY.value
     };
   },
 
+  /**
+   * Observe and react to programmatic SelectionChange notifications.
+   */
   notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
+    // Cancel any in-progress / deferred _closeSelection() action.
+    this._cancelDeferredCloseSelection();
+
     // Ignore selectionChange notifications during handle movements
     if (this._draggingHandles) {
       return;
     }
 
     // If the selection was collapsed to Start or to End, always close it
     if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
         (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
       this._closeSelection();
       return;
     }
 
-    // If selected text no longer exists, close
+    // If selected text no longer exists, schedule a deferred close action.
     if (!aSelection.toString()) {
-      this._closeSelection();
+      this._deferCloseSelection();
+      return;
     }
+
+    // Update the selection handle positions.
+    this._updateCacheForSelection();
+    this._positionHandles();
   },
 
   /*
    * Called from browser.js when the user long taps on text or chooses
    * the "Select Word" context menu item. Initializes SelectionHandler,
    * starts a selection, and positions the text selection handles.
    *
    * @param aOptions list of options describing how to start selection
@@ -299,90 +338,102 @@ var SelectionHandler = {
    *                   x    - The x-coordinate for SELECT_AT_POINT.
    *                   y    - The y-coordinate for SELECT_AT_POINT.
    */
   startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) {
     // Clear out any existing active selection
     this._closeSelection();
 
     if (this._isNonTextInputElement(aElement)) {
-      return false;
+      return this.START_ERROR_NONTEXT_INPUT;
     }
 
     this._initTargetInfo(aElement, this.TYPE_SELECTION);
 
     // Perform the appropriate selection method, if we can't determine method, or it fails, return
-    if (!this._performSelection(aOptions)) {
+    let selectionResult = this._performSelection(aOptions);
+    if (selectionResult !== this.ERROR_NONE) {
       this._deactivate();
-      return false;
+      return selectionResult;
     }
 
     // Double check results of successful selection operation
     let selection = this._getSelection();
-    if (!selection || selection.rangeCount == 0 || selection.getRangeAt(0).collapsed) {
+    if (!selection ||
+        selection.rangeCount == 0 ||
+        selection.getRangeAt(0).collapsed ||
+        this._getSelectedText().length == 0) {
       this._deactivate();
-      return false;
+      return this.START_ERROR_NO_SELECTION;
     }
 
     // Add a listener to end the selection if it's removed programatically
     selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);
     this._activeType = this.TYPE_SELECTION;
 
     // Initialize the cache
     this._cache = { anchorPt: {}, focusPt: {}};
     this._updateCacheForSelection();
 
     let scroll = this._getScrollPos();
     // Figure out the distance between the selection and the click
     let positions = this._getHandlePositions(scroll);
 
-    if (aOptions.mode == this.SELECT_AT_POINT && !this._selectionNearClick(scroll.X + aOptions.x,
-                                                                      scroll.Y + aOptions.y,
-                                                                      positions)) {
+    if (aOptions.mode == this.SELECT_AT_POINT &&
+        !this._selectionNearClick(scroll.X + aOptions.x, scroll.Y + aOptions.y, positions)) {
         this._closeSelection();
-        return false;
+        return this.START_ERROR_PROXIMITY;
     }
 
     // Determine position and show handles, open actionbar
     this._positionHandles(positions);
     Messaging.sendRequest({
       type: "TextSelection:ShowHandles",
       handles: [this.HANDLE_TYPE_ANCHOR, this.HANDLE_TYPE_FOCUS]
     });
     this._updateMenu();
-    return true;
+    return this.ERROR_NONE;
   },
 
   /*
    * Called to perform a selection operation, given a target element, selection method, starting point etc.
    */
   _performSelection: function sh_performSelection(aOptions) {
     if (aOptions.mode == this.SELECT_AT_POINT) {
       // Clear any ranges selected outside SelectionHandler, by code such as Find-In-Page.
       this._contentWindow.getSelection().removeAllRanges();
-      if (!this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
-        return false;
+      try {
+        if (!this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
+          return this.START_ERROR_NO_WORD_SELECTED;
+        }
+      } catch (e) {
+        return this.START_ERROR_SELECT_WORD_FAILED;
       }
 
       // Perform additional phone-number "smart selection".
       if (this._isPhoneNumber(this._getSelection().toString())) {
         this._selectSmartPhoneNumber();
       }
 
-      return true;
+      return this.ERROR_NONE;
     }
 
+    // Only selectAll() assumed from this point.
     if (aOptions.mode != this.SELECT_ALL) {
-      Cu.reportError("SelectionHandler.js: _performSelection() Invalid selection mode " + aOptions.mode);
-      return false;
+      return this.START_ERROR_INVALID_MODE;
     }
 
     // HTMLPreElement is a #text node, SELECT_ALL implies entire paragraph
     if (this._targetElement instanceof HTMLPreElement)  {
-      return this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
+      try {
+        this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
+        return this.ERROR_NONE;
+      } catch (e) {
+        return this.START_ERROR_SELECT_ALL_PARAGRAPH_FAILED;
+      }
     }
 
     // Else default to selectALL Document
     let editor = this._getEditor();
     if (editor) {
       editor.selectAll();
     } else {
       this._getSelectionController().selectAll();
@@ -399,17 +450,17 @@ var SelectionHandler = {
       try {
         selection.extend(lastNode, lastNode.length);
       } catch (e) {
         Cu.reportError("SelectionHandler.js: _performSelection() whitespace trim fails: lastNode[" + lastNode +
           "] lastNode.length[" + lastNode.length + "]");
       }
     }
 
-    return true;
+    return this.ERROR_NONE;
   },
 
   /*
    * Called to expand a selection that appears to represent a phone number. This enhances the basic
    * SELECT_WORDNOSPACE logic employed in performSelection() in response to long-tap / selecting text.
    */
   _selectSmartPhoneNumber: function() {
     this._extendPhoneNumberSelection("forward");
@@ -703,17 +754,17 @@ var SelectionHandler = {
    * Called by BrowserEventHandler when the user taps in a form input.
    * Initializes SelectionHandler and positions the caret handle.
    *
    * @param aX, aY tap location in client coordinates.
    */
   attachCaret: function sh_attachCaret(aElement) {
     // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
     if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
-      return false;
+      return this.ATTACH_ERROR_INCOMPATIBLE;
     }
 
     this._initTargetInfo(aElement, this.TYPE_CURSOR);
 
     // Caret-specific observer/listeners
     BrowserApp.deck.addEventListener("keyup", this, false);
     BrowserApp.deck.addEventListener("compositionupdate", this, false);
     BrowserApp.deck.addEventListener("compositionend", this, false);
@@ -723,17 +774,17 @@ var SelectionHandler = {
     // Determine position and show caret, open actionbar
     this._positionHandles();
     Messaging.sendRequest({
       type: "TextSelection:ShowHandles",
       handles: [this.HANDLE_TYPE_CARET]
     });
     this._updateMenu();
 
-    return true;
+    return this.ERROR_NONE;
   },
 
   // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
   _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
     this._targetElement = aElement;
     if (aElement instanceof Ci.nsIDOMNSEditableElement) {
       if (aSelectionType === this.TYPE_SELECTION) {
         // Blur the targetElement to force IME code to undo previous style compositions
@@ -975,38 +1026,89 @@ var SelectionHandler = {
   callSelection: function sh_callSelection() {
     let selectedText = this._getSelectedPhoneNumber();
     if (selectedText) {
       BrowserApp.loadURI("tel:" + selectedText);
     }
     this._closeSelection();
   },
 
+  /**
+   * Deferred _closeSelection() actions allow for brief periods where programmatic
+   * selection changes have effectively closed the selection, but we anticipate further
+   * activity that may restore it.
+   *
+   * At this point, we hide the UI handles, and stop responding to messages until
+   * either the final _closeSelection() is triggered, or until our Gecko selectionListener
+   * notices a subsequent programmatic selection that results in a new selection.
+   */
+  _deferCloseSelection: function() {
+    // Schedule the deferred _closeSelection() action.
+    this._deferCloseTimer = setTimeout((function() {
+      // Time is up! Close the selection.
+      this._deferCloseTimer = null;
+      this._closeSelection();
+    }).bind(this), DEFER_CLOSE_TRIGGER_MS);
+
+    // Hide any handles while deferClosed.
+    if (this._prevHandlePositions.length) {
+      let positions = this._prevHandlePositions;
+      for (let i in positions) {
+        positions[i].hidden = true;
+      }
+
+      Messaging.sendRequest({
+        type: "TextSelection:PositionHandles",
+        positions: positions,
+        rtl: this._isRTL
+      });
+    }
+  },
+
+  /**
+   * Cancel any current deferred _closeSelection() action.
+   */
+  _cancelDeferredCloseSelection: function() {
+    if (this._deferCloseTimer) {
+      clearTimeout(this._deferCloseTimer);
+      this._deferCloseTimer = null;
+    }
+  },
+
   /*
    * Shuts SelectionHandler down.
    */
   _closeSelection: function sh_closeSelection() {
     // Bail if there's no active selection
     if (this._activeType == this.TYPE_NONE)
       return;
 
     if (this._activeType == this.TYPE_SELECTION)
       this._clearSelection();
 
     this._deactivate();
   },
 
   _clearSelection: function sh_clearSelection() {
+    // Cancel any in-progress / deferred _closeSelection() process.
+    this._cancelDeferredCloseSelection();
+
     let selection = this._getSelection();
     if (selection) {
       // Remove our listener before we clear the selection
       selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this);
-      // Clear selection without clearing the anchorNode or focusNode
+
+      // Remove the selection. For editables, we clear selection without losing
+      // element focus. For non-editables, just clear all.
       if (selection.rangeCount != 0) {
-        selection.collapseToStart();
+        if (this.isElementEditableText(this._targetElement)) {
+          selection.collapseToStart();
+        } else {
+          selection.removeAllRanges();
+        }
       }
     }
   },
 
   _deactivate: function sh_deactivate() {
     this._stopDraggingHandles();
     // Hide handle/caret, close actionbar
     Messaging.sendRequest({ type: "TextSelection:HideHandles" });
@@ -1165,16 +1267,21 @@ var SelectionHandler = {
     let currTargetElementHasText = (this._targetElement.textLength > 0);
     if (currTargetElementHasText != this._prevTargetElementHasText) {
       this._prevTargetElementHasText = currTargetElementHasText;
       this._updateMenu();
     }
   },
 
   subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
+    // Ignore all but selectionListener notifications during deferred _closeSelection().
+    if (this._deferCloseTimer) {
+      return;
+    }
+
     if (this._activeType == this.TYPE_NONE) {
       return;
     }
     let scrollView = aElement.ownerDocument.defaultView;
     let view = this._contentWindow;
     while (true) {
       if (view == scrollView) {
         // The selection is in a view (or sub-view) of the view that scrolled.
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2602,24 +2602,30 @@ var NativeWindow = {
       // If no context-menu for long-press event, it may be meant to trigger text-selection.
       this.menus = null;
       Services.obs.notifyObservers(
         {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
 
       if (SelectionHandler.canSelect(this._target)) {
         // If textSelection WORD is successful,
         // consume / preventDefault the context menu event.
-        if (SelectionHandler.startSelection(this._target,
-          { mode: SelectionHandler.SELECT_AT_POINT, x: event.clientX, y: event.clientY })) {
+        let selectionResult = SelectionHandler.startSelection(this._target,
+          { mode: SelectionHandler.SELECT_AT_POINT,
+            x: event.clientX,
+            y: event.clientY
+          }
+        );
+        if (selectionResult === SelectionHandler.ERROR_NONE) {
           event.preventDefault();
           return;
         }
+
         // If textSelection caret-attachment is successful,
         // consume / preventDefault the context menu event.
-        if (SelectionHandler.attachCaret(this._target)) {
+        if (SelectionHandler.attachCaret(this._target) === SelectionHandler.ERROR_NONE) {
           event.preventDefault();
           return;
         }
       }
     },
 
     // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
     _getTitle: function(node) {
@@ -5085,17 +5091,20 @@ var BrowserEventHandler = {
         this._cancelTapHighlight();
         break;
 
       case "Gesture:SingleTap": {
         try {
           // If the element was previously focused, show the caret attached to it.
           let element = this._highlightElement;
           if (element && element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser)) {
-            SelectionHandler.attachCaret(element);
+            let result = SelectionHandler.attachCaret(element);
+            if (result !== SelectionHandler.ERROR_NONE) {
+              dump("Unexpected failure during caret attach: " + result);
+            }
           }
         } catch(e) {
           Cu.reportError(e);
         }
 
         // The _highlightElement was chosen after fluffing the touch events
         // that led to this SingleTap, so by fluffing the mouse events, they
         // should find the same target since we fluff them again below.
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -441,17 +441,18 @@
             <button id="update-selected-btn" hidden="true"
                     label="&updates.updateSelected.label;"
                     tooltiptext="&updates.updateSelected.tooltip;"/>
           </hbox>
           <richlistbox id="updates-list" class="list" flex="1"/>
         </vbox>
 
         <!-- detail view -->
-        <scrollbox id="detail-view" flex="1" class="view-pane addon-view" orient="vertical" tabindex="0">
+        <scrollbox id="detail-view" flex="1" class="view-pane addon-view" orient="vertical" tabindex="0"
+                   role="document">
           <!-- global warnings -->
           <hbox class="global-warning-container global-warning">
             <hbox class="global-warning-safemode" flex="1" align="center"
                   tooltiptext="&warning.safemode.label;">
               <image class="warning-icon"/>
               <label class="global-warning-text" flex="1" crop="end"
                      value="&warning.safemode.label;"/>
             </hbox>