Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Sat, 26 Apr 2014 20:17:07 -0400
changeset 199840 3d4f2d15fa62f002ec56f46fbc6f97c25e2b3482
parent 199818 e7d3a8320289931c9d69cc423502115c0482053f (current diff)
parent 199839 4af2b4c97db50b3db7d9834d5e7fbb81c9d95d6d (diff)
child 199874 cfb0cadb0283a0951796804933a303be5f01e5f6
push id486
push userasasaki@mozilla.com
push dateMon, 14 Jul 2014 18:39:42 +0000
treeherdermozilla-release@d33428174ff1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone31.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.
addon-sdk/source/lib/sdk/tabs/tabs.js
browser/components/customizableui/test/browser_947914_button_addons.js
browser/components/customizableui/test/browser_947914_button_copy.js
browser/components/customizableui/test/browser_947914_button_cut.js
browser/components/customizableui/test/browser_947914_button_find.js
browser/components/customizableui/test/browser_947914_button_history.js
browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js
browser/components/customizableui/test/browser_947914_button_newWindow.js
browser/components/customizableui/test/browser_947914_button_paste.js
browser/components/customizableui/test/browser_947914_button_print.js
browser/components/customizableui/test/browser_947914_button_savePage.js
browser/components/customizableui/test/browser_947914_button_zoomIn.js
browser/components/customizableui/test/browser_947914_button_zoomOut.js
browser/components/customizableui/test/browser_947914_button_zoomReset.js
--- a/addon-sdk/source/lib/sdk/clipboard.js
+++ b/addon-sdk/source/lib/sdk/clipboard.js
@@ -76,18 +76,21 @@ exports.set = function(aData, aDataType)
   // data URL to detect a better datatype
   if (aData && (!aDataType || aDataType === "image")) {
     try {
       let dataURL = new DataURL(aData);
 
       options.datatype = dataURL.mimeType;
       options.data = dataURL.data;
     }
-    catch (e if e.name === "URIError") {
-      // Not a valid Data URL
+    catch (e) {
+      // Ignore invalid URIs
+      if (e.name !== "URIError") {
+        throw e;
+      }
     }
   }
 
   options = apiUtils.validateOptions(options, {
     data: {
       is: ["string"]
     },
     datatype: {
--- a/addon-sdk/source/lib/sdk/console/plain-text.js
+++ b/addon-sdk/source/lib/sdk/console/plain-text.js
@@ -7,17 +7,17 @@
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const self = require("../self");
 const prefs = require("../preferences/service");
 const { merge } = require("../util/object");
-const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
+const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 
 const DEFAULT_LOG_LEVEL = "error";
 const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel";
 const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel";
 
 let logLevel = DEFAULT_LOG_LEVEL;
 function setLogLevel() {
   logLevel = prefs.get(ADDON_LOG_LEVEL_PREF,
@@ -39,22 +39,23 @@ let logLevelObserver = {
   }
 };
 let branch = Cc["@mozilla.org/preferences-service;1"].
              getService(Ci.nsIPrefService).
              getBranch(null);
 branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true);
 branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true);
 
-function PlainTextConsole(print) {
+function PlainTextConsole(print, innerID) {
 
   let consoleOptions = {
     prefix: self.name + ": ",
     maxLogLevel: logLevel,
-    dump: print
+    dump: print,
+    innerID: innerID
   };
   let console = new ConsoleAPI(consoleOptions);
 
   // As we freeze the console object, we can't modify this property afterward
   Object.defineProperty(console, "maxLogLevel", {
     get: function() {
       return logLevel;
     }
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -14,16 +14,17 @@ const { requiresAddonGlobal } = require(
 const { delay: async } = require('../lang/functional');
 const { Ci, Cu, Cc } = require('chrome');
 const timer = require('../timers');
 const { URL } = require('../url');
 const { sandbox, evaluate, load } = require('../loader/sandbox');
 const { merge } = require('../util/object');
 const { getTabForContentWindow } = require('../tabs/utils');
 const { getInnerId } = require('../window/utils');
+const { PlainTextConsole } = require('../console/plain-text');
 
 // WeakMap of sandboxes so we can access private values
 const sandboxes = new WeakMap();
 
 /* Trick the linker in order to ensure shipping these files in the XPI.
   require('./content-worker.js');
   Then, retrieve URL of these files in the XPI:
 */
@@ -192,18 +193,20 @@ const WorkerSandbox = Class({
     let chromeAPI = createChromeAPI();
     let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
 
     // Merge `emitToContent` and `hasListenerFor` into our private
     // model of the WorkerSandbox so we can communicate with content
     // script
     merge(model, result);
 
+    let console = new PlainTextConsole(null, getInnerId(window));
+
     // Handle messages send by this script:
-    setListeners(this);
+    setListeners(this, console);
 
     // Inject `addon` global into target document if document is trusted,
     // `addon` in document is equivalent to `self` in content script.
     if (requiresAddonGlobal(worker)) {
       Object.defineProperty(getUnsafeWindow(window), 'addon', {
           value: content.self
         }
       );
@@ -299,17 +302,17 @@ function importScripts (workerSandbox, .
         throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
     }
     catch(e) {
       emit(worker, 'error', e);
     }
   }
 }
 
-function setListeners (workerSandbox) {
+function setListeners (workerSandbox, console) {
   let { worker } = modelFor(workerSandbox);
   // console.xxx calls
   workerSandbox.on('console', function consoleListener (kind, ...args) {
     console[kind].apply(console, args);
   });
 
   // self.postMessage calls
   workerSandbox.on('message', function postMessage(data) {
--- a/addon-sdk/source/lib/sdk/core/promise.js
+++ b/addon-sdk/source/lib/sdk/core/promise.js
@@ -1,266 +1,51 @@
-;(function(id, factory) { // Module boilerplate :(
-  if (typeof(define) === 'function') { // RequireJS
-    define(factory);
-  } else if (typeof(require) === 'function') { // CommonJS
-    factory.call(this, require, exports, module);
-  } else if (String(this).indexOf('BackstagePass') >= 0) { // JSM
-    this[factory.name] = {};
-    try {
-      this.console = this['Components'].utils
-          .import('resource://gre/modules/devtools/Console.jsm', {}).console;
-    }
-    catch (ex) {
-      // Avoid failures on different toolkit configurations.
-    }
-    factory(function require(uri) {
-      var imports = {};
-      this['Components'].utils.import(uri, imports);
-      return imports;
-    }, this[factory.name], { uri: __URI__, id: id });
-    this.EXPORTED_SYMBOLS = [factory.name];
-  } else if (~String(this).indexOf('Sandbox')) { // Sandbox
-    factory(function require(uri) {}, this, { id: id });
-  } else {  // Browser or alike
-    var globals = this;
-    factory(function require(id) {
-      return globals[id];
-    }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id });
-  }
-}).call(this, 'promise/core', function Promise(require, exports, module) {
+/* 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';
 
+/*
+ * Uses `Promise.jsm` as a core implementation, with additional sugar
+ * from previous implementation, with inspiration from `Q` and `when`
+ *
+ * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm
+ * https://github.com/cujojs/when
+ * https://github.com/kriskowal/q
+ */
+const PROMISE_URI = 'resource://gre/modules/Promise.jsm';
+
+getEnvironment.call(this, function ({ require, exports, module, Cu }) {
+
+const { defer, resolve, all, reject, race } = Cu.import(PROMISE_URI, {}).Promise;
+
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'unstable'
 };
 
-/**
- * Internal utility: Wraps given `value` into simplified promise, successfully
- * fulfilled to a given `value`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function fulfilled(value) {
-  return { then: function then(fulfill) { fulfill(value); } };
-}
-
-/**
- * Internal utility: Wraps given input into simplified promise, pre-rejected
- * with a given `reason`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function rejected(reason) {
-  return { then: function then(fulfill, reject) { reject(reason); } };
-}
-
-/**
- * Internal utility: Returns `true` if given `value` is a promise. Value is
- * assumed to be a promise if it implements method `then`.
- */
-function isPromise(value) {
-  return value && typeof(value.then) === 'function';
-}
-
-/**
- * Creates deferred object containing fresh promise & methods to either resolve
- * or reject it. The result is an object with the following properties:
- * - `promise` Eventual value representation implementing CommonJS [Promises/A]
- *   (http://wiki.commonjs.org/wiki/Promises/A) API.
- * - `resolve` Single shot function that resolves enclosed `promise` with a
- *   given `value`.
- * - `reject` Single shot function that rejects enclosed `promise` with a given
- *   `reason`.
- *
- * An optional `prototype` argument is used as a prototype of the returned
- * `promise` allowing one to implement additional API. If prototype is not
- * passed then it falls back to `Object.prototype`.
- *
- *  ## Example
- *
- *  function fetchURI(uri, type) {
- *    var deferred = defer();
- *    var request = new XMLHttpRequest();
- *    request.open("GET", uri, true);
- *    request.responseType = type;
- *    request.onload = function onload() {
- *      deferred.resolve(request.response);
- *    }
- *    request.onerror = function(event) {
- *     deferred.reject(event);
- *    }
- *    request.send();
- *
- *    return deferred.promise;
- *  }
- */
-function defer(prototype) {
-  // Define FIFO queue of observer pairs. Once promise is resolved & all queued
-  // observers are forwarded to `result` and variable is set to `null`.
-  var observers = [];
-
-  // Promise `result`, which will be assigned a resolution value once promise
-  // is resolved. Note that result will always be assigned promise (or alike)
-  // object to take care of propagation through promise chains. If result is
-  // `null` promise is not resolved yet.
-  var result = null;
-
-  prototype = (prototype || prototype === null) ? prototype : Object.prototype;
-
-  // Create an object implementing promise API.
-  var promise = Object.create(prototype, {
-    then: { value: function then(onFulfill, onError) {
-      var deferred = defer(prototype);
-
-      function resolve(value) {
-        // If `onFulfill` handler is provided resolve `deferred.promise` with
-        // result of invoking it with a resolution value. If handler is not
-        // provided propagate value through.
-        try {
-          deferred.resolve(onFulfill ? onFulfill(value) : value);
-        }
-        // `onFulfill` may throw exception in which case resulting promise
-        // is rejected with thrown exception.
-        catch(error) {
-          if (exports._reportErrors && typeof(console) === 'object')
-            console.error(error);
-          // Note: Following is equivalent of `deferred.reject(error)`,
-          // we use this shortcut to reduce a stack.
-          deferred.resolve(rejected(error));
-        }
-      }
-
-      function reject(reason) {
-        try {
-          if (onError) deferred.resolve(onError(reason));
-          else deferred.resolve(rejected(reason));
-        }
-        catch(error) {
-          if (exports._reportErrors && typeof(console) === 'object')
-            console.error(error);
-          deferred.resolve(rejected(error));
-        }
-      }
-
-      // If enclosed promise (`this.promise`) observers queue is still alive
-      // enqueue a new observer pair into it. Note that this does not
-      // necessary means that promise is pending, it may already be resolved,
-      // but we still have to queue observers to guarantee an order of
-      // propagation.
-      if (observers) {
-        observers.push({ resolve: resolve, reject: reject });
-      }
-      // Otherwise just forward observer pair right to a `result` promise.
-      else {
-        result.then(resolve, reject);
-      }
-
-      return deferred.promise;
-    }}
-  })
-
-  var deferred = {
-    promise: promise,
-    /**
-     * Resolves associated `promise` to a given `value`, unless it's already
-     * resolved or rejected. Note that resolved promise is not necessary a
-     * successfully fulfilled. Promise may be resolved with a promise `value`
-     * in which case `value` promise's fulfillment / rejection will propagate
-     * up to a promise resolved with `value`.
-     */
-    resolve: function resolve(value) {
-      if (!result) {
-        // Store resolution `value` in a `result` as a promise, so that all
-        // the subsequent handlers can be simply forwarded to it. Since
-        // `result` will be a promise all the value / error propagation will
-        // be uniformly taken care of.
-        result = isPromise(value) ? value : fulfilled(value);
-
-        // Forward already registered observers to a `result` promise in the
-        // order they were registered. Note that we intentionally dequeue
-        // observer at a time until queue is exhausted. This makes sure that
-        // handlers registered as side effect of observer forwarding are
-        // queued instead of being invoked immediately, guaranteeing FIFO
-        // order.
-        while (observers.length) {
-          var observer = observers.shift();
-          result.then(observer.resolve, observer.reject);
-        }
-
-        // Once `observers` queue is exhausted we `null`-ify it, so that
-        // new handlers are forwarded straight to the `result`.
-        observers = null;
-      }
-    },
-    /**
-     * Rejects associated `promise` with a given `reason`, unless it's already
-     * resolved / rejected. This is just a (better performing) convenience
-     * shortcut for `deferred.resolve(reject(reason))`.
-     */
-    reject: function reject(reason) {
-      // Note that if promise is resolved that does not necessary means that it
-      // is successfully fulfilled. Resolution value may be a promise in which
-      // case its result propagates. In other words if promise `a` is resolved
-      // with promise `b`, `a` is either fulfilled or rejected depending
-      // on weather `b` is fulfilled or rejected. Here `deferred.promise` is
-      // resolved with a promise pre-rejected with a given `reason`, there for
-      // `deferred.promise` is rejected with a given `reason`. This may feel
-      // little awkward first, but doing it this way greatly simplifies
-      // propagation through promise chains.
-      deferred.resolve(rejected(reason));
-    }
-  };
-
-  return deferred;
-}
-exports.defer = defer;
-
-/**
- * Returns a promise resolved to a given `value`. Optionally a second
- * `prototype` argument may be provided to be used as a prototype for the
- * returned promise.
- */
-function resolve(value, prototype) {
-  var deferred = defer(prototype);
-  deferred.resolve(value);
-  return deferred.promise;
-}
-exports.resolve = resolve;
-
-/**
- * Returns a promise rejected with a given `reason`. Optionally a second
- * `prototype` argument may be provided to be used as a prototype for the
- * returned promise.
- */
-function reject(reason, prototype) {
-  var deferred = defer(prototype);
-  deferred.reject(reason);
-  return deferred.promise;
-}
-exports.reject = reject;
-
-var promised = (function() {
+let promised = (function() {
   // Note: Define shortcuts and utility functions here in order to avoid
   // slower property accesses and unnecessary closure creations on each
   // call of this popular function.
 
   var call = Function.call;
   var concat = Array.prototype.concat;
 
   // Utility function that does following:
   // execute([ f, self, args...]) => f.apply(self, args)
-  function execute(args) { return call.apply(call, args) }
+  function execute (args) call.apply(call, args)
 
   // Utility function that takes promise of `a` array and maybe promise `b`
   // as arguments and returns promise for `a.concat(b)`.
   function promisedConcat(promises, unknown) {
-    return promises.then(function(values) {
-      return resolve(unknown).then(function(value) {
-        return values.concat([ value ]);
-      });
+    return promises.then(function (values) {
+      return resolve(unknown)
+        .then(function (value) values.concat([value]));
     });
   }
 
   return function promised(f, prototype) {
     /**
     Returns a wrapped `f`, which when called returns a promise that resolves to
     `f(...)` passing all the given arguments to it, which by the way may be
     promises. Optionally second `prototype` argument may be provided to be used
@@ -275,16 +60,58 @@ var promised = (function() {
     return function promised() {
       // create array of [ f, this, args... ]
       return concat.apply([ f, this ], arguments).
         // reduce it via `promisedConcat` to get promised array of fulfillments
         reduce(promisedConcat, resolve([], prototype)).
         // finally map that to promise of `f.apply(this, args...)`
         then(execute);
     };
-  }
+  };
 })();
+
 exports.promised = promised;
-
-var all = promised(Array);
 exports.all = all;
+exports.defer = defer;
+exports.resolve = resolve;
+exports.reject = reject;
+exports.race = race;
+exports.Promise = Promise;
 
 });
+
+function getEnvironment (callback) {
+  let Cu, _exports, _module, _require;
+
+  // CommonJS / SDK
+  if (typeof(require) === 'function') {
+    Cu = require('chrome').Cu;
+    _exports = exports;
+    _module = module;
+    _require = require;
+  }
+  // JSM
+  else if (String(this).indexOf('BackstagePass') >= 0) {
+    Cu = this['Components'].utils;
+    _exports = this.Promise = {};
+    _module = { uri: __URI__, id: 'promise/core' };
+    _require = uri => {
+      let imports = {};
+      Cu.import(uri, imports);
+      return imports;
+    };
+    this.EXPORTED_SYMBOLS = ['Promise'];
+  // mozIJSSubScriptLoader.loadSubscript
+  } else if (~String(this).indexOf('Sandbox')) {
+    Cu = this['Components'].utils;
+    _exports = this;
+    _module = { id: 'promise/core' };
+    _require = uri => {};
+  }
+
+  callback({
+    Cu: Cu,
+    exports: _exports,
+    module: _module,
+    require: _require
+  });
+}
+
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/model/core.js
@@ -0,0 +1,25 @@
+/* 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";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { dispatcher } = require("../util/dispatcher");
+
+
+// Define `modelFor` accessor function that can be implemented
+// for different types of views. Since view's we'll be dealing
+// with types that don't really play well with `instanceof`
+// operator we're gonig to use `dispatcher` that is slight
+// extension over polymorphic dispatch provided by method.
+// This allows models to extend implementations of this by
+// providing predicates:
+//
+// modelFor.when($ => $ && $.nodeName === "tab", findTabById($.id))
+const modelFor = dispatcher("modelFor");
+exports.modelFor = modelFor;
--- a/addon-sdk/source/lib/sdk/net/url.js
+++ b/addon-sdk/source/lib/sdk/net/url.js
@@ -10,37 +10,34 @@ module.metadata = {
 
 const { Cu, components } = require("chrome");
 const { defer } = require("../core/promise");
 const { merge } = require("../util/object");
 
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 
 /**
- * Open a channel synchronously for the URI given, with an optional charset, and
- * returns a resolved promise if succeed; rejected promise otherwise.
+ * Reads a URI and returns a promise.
+ *
+ * @param uri {string} The URI to read
+ * @param [options] {object} This parameter can have any or all of the following
+ * fields: `charset`. By default the `charset` is set to 'UTF-8'.
+ *
+ * @returns {promise}  The promise that will be resolved with the content of the
+ *          URL given.
+ *
+ * @example
+ *  let promise = readURI('resource://gre/modules/NetUtil.jsm', {
+ *    charset: 'US-ASCII'
+ *  });
  */
-function readSync(uri, charset) {
-  let { promise, resolve, reject } = defer();
+function readURI(uri, options) {
+  options = options || {};
+  let charset = options.charset || 'UTF-8';
 
-  try {
-    resolve(readURISync(uri, charset));
-  }
-  catch (e) {
-    reject("Failed to read: '" + uri + "' (Error Code: " + e.result + ")");
-  }
-
-  return promise;
-}
-
-/**
- * Open a channel synchronously for the URI given, with an optional charset, and
- * returns a promise.
- */
-function readAsync(uri, charset) {
   let channel = NetUtil.newChannel(uri, charset, null);
 
   let { promise, resolve, reject } = defer();
 
   try {
     NetUtil.asyncFetch(channel, function (stream, result) {
       if (components.isSuccessCode(result)) {
         let count = stream.available();
@@ -54,44 +51,16 @@ function readAsync(uri, charset) {
   }
   catch (e) {
     reject("Failed to read: '" + uri + "' (Error: " + e.message + ")");
   }
 
   return promise;
 }
 
-/**
- * Reads a URI and returns a promise. If the `sync` option is set to `true`, the
- * promise will be resolved synchronously.
- *
- * @param uri {string} The URI to read
- * @param [options] {object} This parameter can have any or all of the following
- * fields: `sync`, `charset`. By default the `charset` is set to 'UTF-8'.
- *
- * @returns {promise}  The promise that will be resolved with the content of the
- *          URL given.
-  *
- * @example
- *  let promise = readURI('resource://gre/modules/NetUtil.jsm', {
- *    sync: true,
- *    charset: 'US-ASCII'
- });
- */
-function readURI(uri, options) {
-  options = merge({
-    charset: "UTF-8",
-    sync: false
-  }, options);
-
-  return options.sync
-    ? readSync(uri, options.charset)
-    : readAsync(uri, options.charset);
-}
-
 exports.readURI = readURI;
 
 /**
  * Reads a URI synchronously.
  * This function is intentionally undocumented to favorites the `readURI` usage.
  *
  * @param uri {string} The URI to read
  * @param [charset] {string} The character set to use when read the content of
--- a/addon-sdk/source/lib/sdk/notifications.js
+++ b/addon-sdk/source/lib/sdk/notifications.js
@@ -40,25 +40,26 @@ exports.notify = function notifications_
   };
   function notifyWithOpts(notifyFn) {
     notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver,
              valOpts.data, clickObserver, valOpts.tag, valOpts.dir, valOpts.lang);
   }
   try {
     notifyWithOpts(notify);
   }
-  catch (err if err instanceof Ci.nsIException &&
-                err.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
-    console.warn("The notification icon named by " + valOpts.iconURL +
-                 " does not exist.  A default icon will be used instead.");
-    delete valOpts.iconURL;
-    notifyWithOpts(notify);
-  }
   catch (err) {
-    notifyWithOpts(notifyUsingConsole);
+    if (err instanceof Ci.nsIException && err.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
+      console.warn("The notification icon named by " + valOpts.iconURL +
+                   " does not exist.  A default icon will be used instead.");
+      delete valOpts.iconURL;
+      notifyWithOpts(notify);
+    }
+    else {
+      notifyWithOpts(notifyUsingConsole);
+    }
   }
 };
 
 function notifyUsingConsole(iconURL, title, text) {
   title = title ? "[" + title + "]" : "";
   text = text || "";
   let str = [title, text].filter(function (s) s).join(" ");
   console.log(str);
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -362,21 +362,26 @@ function style(panel) {
     let window = document.defaultView;
     let node = document.getAnonymousElementByAttribute(panel, "class",
                                                        "panel-arrowcontent") ||
                // Before bug 764755, anonymous content was different:
                // TODO: Remove this when targeting FF16+
                 document.getAnonymousElementByAttribute(panel, "class",
                                                         "panel-inner-arrowcontent");
 
-    let color = window.getComputedStyle(node).getPropertyValue("color");
+    let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node);
 
     let style = contentDocument.createElement("style");
     style.id = "sdk-panel-style";
-    style.textContent = "body { color: " + color + "; }";
+    style.textContent = "body { " +
+      "color: " + color + ";" +
+      "font-family: " + fontFamily + ";" +
+      "font-weight: " + fontWeight + ";" +
+      "font-size: " + fontSize + ";" +
+    "}";
 
     let container = contentDocument.head ? contentDocument.head :
                     contentDocument.documentElement;
 
     if (container.firstChild)
       container.insertBefore(style, container.firstChild);
     else
       container.appendChild(style);
--- a/addon-sdk/source/lib/sdk/places/utils.js
+++ b/addon-sdk/source/lib/sdk/places/utils.js
@@ -44,18 +44,19 @@ let TreeNode = Class({
   get: method(get),
   walk: method(walk),
   toString: function () '[object TreeNode]'
 });
 exports.TreeNode = TreeNode;
 
 /*
  * Descends down from `node` applying `fn` to each in order.
- * Can be asynchronous if `fn` returns a promise. `fn` is passed 
- * one argument, the current node, `curr`
+ * `fn` can return values or promises -- if promise returned,
+ * children are not processed until resolved. `fn` is passed 
+ * one argument, the current node, `curr`.
  */
 function walk (curr, fn) {
   return promised(fn)(curr).then(val => {
     return all(curr.children.map(child => walk(child, fn)));
   });
 } 
 
 /*
--- a/addon-sdk/source/lib/sdk/preferences/service.js
+++ b/addon-sdk/source/lib/sdk/preferences/service.js
@@ -132,24 +132,28 @@ exports.keys = keys;
 function isSet(name) {
   return (has(name) && prefSvc.prefHasUserValue(name));
 }
 exports.isSet = isSet;
 
 function reset(name) {
   try {
     prefSvc.clearUserPref(name);
-  } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) {
+  }
+  catch (e) {
     // The pref service throws NS_ERROR_UNEXPECTED when the caller tries
     // to reset a pref that doesn't exist or is already set to its default
     // value.  This interface fails silently in those cases, so callers
     // can unconditionally reset a pref without having to check if it needs
     // resetting first or trap exceptions after the fact.  It passes through
     // other exceptions, however, so callers know about them, since we don't
     // know what other exceptions might be thrown and what they might mean.
+    if (e.result != Cr.NS_ERROR_UNEXPECTED) {
+      throw e;
+    }
   }
 }
 exports.reset = reset;
 
 function getLocalized(name, defaultValue) {
   let value = null;
   try {
     value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
--- a/addon-sdk/source/lib/sdk/simple-storage.js
+++ b/addon-sdk/source/lib/sdk/simple-storage.js
@@ -3,23 +3,26 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const { Cc, Ci } = require("chrome");
+const { Cc, Ci, Cu } = require("chrome");
 const file = require("./io/file");
 const prefs = require("./preferences/service");
 const jpSelf = require("./self");
 const timer = require("./timers");
 const unload = require("./system/unload");
 const { emit, on, off } = require("./event/core");
+const { defer } = require('./core/promise');
+
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
 const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes
 
 const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
 const QUOTA_DEFAULT = 5242880; // 5 MiB
 
 const JETPACK_DIR_BASENAME = "jetpack";
@@ -30,29 +33,78 @@ Object.defineProperties(exports, {
     get: function() { return manager.root; },
     set: function(value) { manager.root = value; }
   },
   quotaUsage: {
     get: function() { return manager.quotaUsage; }
   }
 });
 
+function getHash(data) {
+  let { promise, resolve } = defer();
+
+  let crypto = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+  crypto.init(crypto.MD5);
+
+  let listener = {
+    onStartRequest: function() { },
+
+    onDataAvailable: function(request, context, inputStream, offset, count) {
+      crypto.updateFromStream(inputStream, count);
+    },
+
+    onStopRequest: function(request, context, status) {
+      resolve(crypto.finish(false));
+    }
+  };
+
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                  createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let stream = converter.convertToInputStream(data);
+  let pump = Cc["@mozilla.org/network/input-stream-pump;1"].
+             createInstance(Ci.nsIInputStreamPump);
+  pump.init(stream, -1, -1, 0, 0, true);
+  pump.asyncRead(listener, null);
+
+  return promise;
+}
+
+function writeData(filename, data) {
+  let { promise, resolve, reject } = defer();
+
+  let stream = file.open(filename, "w");
+  try {
+    stream.writeAsync(data, err => {
+      if (err)
+        reject(err);
+      else
+        resolve();
+    });
+  }
+  catch (err) {
+    // writeAsync closes the stream after it's done, so only close on error.
+    stream.close();
+    reject(err);
+  }
+
+  return promise;
+}
+
 // A generic JSON store backed by a file on disk.  This should be isolated
 // enough to move to its own module if need be...
 function JsonStore(options) {
   this.filename = options.filename;
   this.quota = options.quota;
   this.writePeriod = options.writePeriod;
   this.onOverQuota = options.onOverQuota;
   this.onWrite = options.onWrite;
-
+  this.hash = null;
   unload.ensure(this);
-
-  this.writeTimer = timer.setInterval(this.write.bind(this),
-                                      this.writePeriod);
+  this.startTimer();
 }
 
 JsonStore.prototype = {
   // The store's root.
   get root() {
     return this.isRootInited ? this._root : {};
   },
 
@@ -76,21 +128,28 @@ JsonStore.prototype = {
   // Percentage of quota used, as a number [0, Inf).  > 1 implies over quota.
   // Undefined if there is no quota.
   get quotaUsage() {
     return this.quota > 0 ?
            JSON.stringify(this.root).length / this.quota :
            undefined;
   },
 
+  startTimer: function JsonStore_startTimer() {
+    timer.setTimeout(() => {
+      this.write().then(this.startTimer.bind(this));
+    }, this.writePeriod);
+  },
+
   // Removes the backing file and all empty subdirectories.
   purge: function JsonStore_purge() {
     try {
       // This'll throw if the file doesn't exist.
       file.remove(this.filename);
+      this.hash = null;
       let parentPath = this.filename;
       do {
         parentPath = file.dirname(parentPath);
         // This'll throw if the dir isn't empty.
         file.rmdir(parentPath);
       } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME);
     }
     catch (err) {}
@@ -100,41 +159,35 @@ JsonStore.prototype = {
   read: function JsonStore_read() {
     try {
       let str = file.read(this.filename);
 
       // Ideally we'd log the parse error with console.error(), but logged
       // errors cause tests to fail.  Supporting "known" errors in the test
       // harness appears to be non-trivial.  Maybe later.
       this.root = JSON.parse(str);
+      let self = this;
+      getHash(str).then(hash => this.hash = hash);
     }
     catch (err) {
       this.root = {};
+      this.hash = null;
     }
   },
 
-  // If the store is under quota, writes the root to the backing file.
-  // Otherwise quota observers are notified and nothing is written.
-  write: function JsonStore_write() {
-    if (this.quotaUsage > 1)
-      this.onOverQuota(this);
-    else
-      this._write();
-  },
-
   // Cleans up on unload.  If unloading because of uninstall, the store is
   // purged; otherwise it's written.
   unload: function JsonStore_unload(reason) {
-    timer.clearInterval(this.writeTimer);
+    timer.clearTimeout(this.writeTimer);
     this.writeTimer = null;
 
     if (reason === "uninstall")
       this.purge();
     else
-      this._write();
+      this.write();
   },
 
   // True if the root is an empty object.
   get _isEmpty() {
     if (this.root && typeof(this.root) === "object") {
       let empty = true;
       for (let key in this.root) {
         empty = false;
@@ -143,42 +196,50 @@ JsonStore.prototype = {
       return empty;
     }
     return false;
   },
 
   // Writes the root to the backing file, notifying write observers when
   // complete.  If the store is over quota or if it's empty and the store has
   // never been written, nothing is written and write observers aren't notified.
-  _write: function JsonStore__write() {
+  write: Task.async(function JsonStore_write() {
     // Don't write if the root is uninitialized or if the store is empty and the
     // backing file doesn't yet exist.
     if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename)))
       return;
 
+    let data = JSON.stringify(this.root);
+
     // If the store is over quota, don't write.  The current under-quota state
     // should persist.
-    if (this.quotaUsage > 1)
+    if ((this.quota > 0) && (data.length > this.quota)) {
+      this.onOverQuota(this);
+      return;
+    }
+
+    // Hash the data to compare it to any previously written data
+    let hash = yield getHash(data);
+
+    if (hash == this.hash)
       return;
 
     // Finally, write.
-    let stream = file.open(this.filename, "w");
     try {
-      stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) {
-        if (err)
-          console.error("Error writing simple storage file: " + this.filename);
-        else if (this.onWrite)
-          this.onWrite(this);
-      }.bind(this));
+      yield writeData(this.filename, data);
+
+      this.hash = hash;
+      if (this.onWrite)
+        this.onWrite(this);
     }
     catch (err) {
-      // writeAsync closes the stream after it's done, so only close on error.
-      stream.close();
+      console.error("Error writing simple storage file: " + this.filename);
+      console.error(err);
     }
-  }
+  })
 };
 
 
 // This manages a JsonStore singleton and tailors its use to simple storage.
 // The root of the JsonStore is lazy-loaded:  The backing file is only read the
 // first time the root's gotten.
 let manager = ({
   jsonStore: null,
--- a/addon-sdk/source/lib/sdk/tabs.js
+++ b/addon-sdk/source/lib/sdk/tabs.js
@@ -1,10 +1,37 @@
 /* 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';
+"use strict";
 
 module.metadata = {
-  'stability': 'stable'
+  "stability": "unstable",
+  "engines": {
+    "Firefox": "*",
+    "Fennec": "*"
+  }
 };
 
-module.exports = require('./tabs/tabs');
+const { modelFor } = require("./model/core");
+const { viewFor } = require("./view/core");
+const { isTab } = require("./tabs/utils");
+
+
+if (require("./system/xul-app").is("Fennec")) {
+  module.exports = require("./windows/tabs-fennec").tabs;
+}
+else {
+  module.exports = require("./tabs/tabs-firefox");
+}
+
+const tabs = module.exports;
+
+// Implement `modelFor` function for the Tab instances.
+// Finds a right model by iterating over all tab models
+// and finding one that wraps given `view`.
+modelFor.when(isTab, view => {
+  for (let model of tabs) {
+    if (viewFor(model) === view)
+      return model;
+  }
+  return null;
+});
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/tabs/tabs.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* 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';
-
-module.metadata = {
-  'stability': 'unstable',
-  'engines': {
-    'Firefox': '*',
-    'Fennec': '*'
-  }
-};
-
-if (require('../system/xul-app').name == 'Fennec') {
-  module.exports = require('../windows/tabs-fennec').tabs;
-}
-else {
-  module.exports = require('./tabs-firefox');
-}
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -15,16 +15,41 @@ const { Ci } = require('chrome');
 const { defer } = require("../lang/functional");
 const { windows, isBrowser } = require('../window/utils');
 const { isPrivateBrowsingSupported } = require('../self');
 const { isGlobalPBSupported } = require('../private-browsing/utils');
 
 // Bug 834961: ignore private windows when they are not supported
 function getWindows() windows(null, { includePrivate: isPrivateBrowsingSupported || isGlobalPBSupported });
 
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+// Define predicate functions that can be used to detech weather
+// we deal with fennec tabs or firefox tabs.
+
+// Predicate to detect whether tab is XUL "Tab" node.
+const isXULTab = tab =>
+  tab instanceof Ci.nsIDOMNode &&
+  tab.nodeName === "tab" &&
+  tab.namespaceURI === XUL_NS;
+exports.isXULTab = isXULTab;
+
+// Predicate to detecet whether given tab is a fettec tab.
+// Unfortunately we have to guess via duck typinng of:
+// http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/browser.js#2583
+const isFennecTab = tab =>
+  tab &&
+  tab.QueryInterface &&
+  Ci.nsIBrowserTab &&
+  tab.QueryInterface(Ci.nsIBrowserTab) === tab;
+exports.isFennecTab = isFennecTab;
+
+const isTab = x => isXULTab(x) || isFennecTab(x);
+exports.isTab = isTab;
+
 function activateTab(tab, window) {
   let gBrowser = getTabBrowserForTab(tab);
 
   // normal case
   if (gBrowser) {
     gBrowser.selectedTab = tab;
   }
   // fennec ?
--- a/addon-sdk/source/lib/sdk/test/loader.js
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { resolveURI, Require,
         unload, override, descriptor } = require('../../toolkit/loader');
 const { ensure } = require('../system/unload');
 const addonWindow = require('../addon/window');
-const { PlainTextConsole } = require("sdk/console/plain-text");
+const { PlainTextConsole } = require('sdk/console/plain-text');
 
 let defaultGlobals = override(require('../system/globals'), {
   console: console
 });
 
 function CustomLoader(module, globals, packaging, overrides={}) {
   let options = packaging || require("@loader/options");
   options = override(options, {
@@ -38,43 +38,53 @@ function CustomLoader(module, globals, p
       unload(loader, reason);
     }
   }));
   ensure(wrapper);
   return wrapper;
 };
 exports.Loader = CustomLoader;
 
+function HookedPlainTextConsole(hook, print, innerID) {
+  this.log = hook.bind(null, "log", innerID);
+  this.info = hook.bind(null, "info", innerID);
+  this.warn = hook.bind(null, "warn", innerID);
+  this.error = hook.bind(null, "error", innerID);
+  this.debug = hook.bind(null, "debug", innerID);
+  this.exception = hook.bind(null, "exception", innerID);
+  this.time = hook.bind(null, "time", innerID);
+  this.timeEnd = hook.bind(null, "timeEnd", innerID);
+
+  this.__exposedProps__ = {
+    log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
+    exception: "rw", time: "rw", timeEnd: "rw"
+  };
+}
+
 // Creates a custom loader instance whose console module is hooked in order
 // to avoid printing messages to the console, and instead, expose them in the
 // returned `messages` array attribute
 exports.LoaderWithHookedConsole = function (module, callback) {
   let messages = [];
-  function hook(msg) {
-    messages.push({type: this, msg: msg});
+  function hook(type, innerID, msg) {
+    messages.push({ type: type, msg: msg, innerID: innerID });
     if (callback)
-      callback(this, msg);
+      callback(type, msg, innerID);
   }
+
   return {
     loader: CustomLoader(module, {
-      console: {
-        log: hook.bind("log"),
-        info: hook.bind("info"),
-        warn: hook.bind("warn"),
-        error: hook.bind("error"),
-        debug: hook.bind("debug"),
-        exception: hook.bind("exception"),
-        time: hook.bind("time"),
-        timeEnd: hook.bind("timeEnd"),
-        __exposedProps__: {
-          log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-          exception: "rw", time: "rw", timeEnd: "rw"
+      console: new HookedPlainTextConsole(hook, null, null)
+    }, override(require("@loader/options"), {
+      modules: {
+        'sdk/console/plain-text': {
+          PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
         }
       }
-    }),
+    })),
     messages: messages
   };
 }
 
 // Same than LoaderWithHookedConsole with lower level, instead we get what is
 // actually printed to the command line console
 exports.LoaderWithHookedConsole2 = function (module, callback) {
   let messages = [];
@@ -89,30 +99,24 @@ exports.LoaderWithHookedConsole2 = funct
     messages: messages
   };
 }
 
 // Creates a custom loader with a filtered console. The callback is passed every
 // console message type and message and if it returns false the message will
 // not be logged normally
 exports.LoaderWithFilteredConsole = function (module, callback) {
-  function hook(msg) {
-    if (callback && callback(this, msg) == false)
+  function hook(type, innerID, msg) {
+    if (callback && callback(type, msg, innerID) == false)
       return;
-    console[this](msg);
+    console[type](msg);
   }
+
   return CustomLoader(module, {
-    console: {
-      log: hook.bind("log"),
-      info: hook.bind("info"),
-      warn: hook.bind("warn"),
-      error: hook.bind("error"),
-      debug: hook.bind("debug"),
-      exception: hook.bind("exception"),
-      time: hook.bind("time"),
-      timeEnd: hook.bind("timeEnd"),
-      __exposedProps__: {
-        log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-        exception: "rw", time: "rw", timeEnd: "rw"
+    console: new HookedPlainTextConsole(hook, null, null)
+  }, override(require("@loader/options"), {
+    modules: {
+      'sdk/console/plain-text': {
+        PlainTextConsole: HookedPlainTextConsole.bind(null, hook)
       }
     }
-  });
+  }));
 }
--- a/addon-sdk/source/lib/sdk/url.js
+++ b/addon-sdk/source/lib/sdk/url.js
@@ -23,32 +23,37 @@ var resProt = ios.getProtocolHandler("re
 var URLParser = Cc["@mozilla.org/network/url-parser;1?auth=no"]
                 .getService(Ci.nsIURLParser);
 
 function newURI(uriStr, base) {
   try {
     let baseURI = base ? ios.newURI(base, null, null) : null;
     return ios.newURI(uriStr, null, baseURI);
   }
-  catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) {
-    throw new Error("malformed URI: " + uriStr);
-  }
-  catch (e if (e.result == Cr.NS_ERROR_FAILURE ||
-               e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) {
-    throw new Error("invalid URI: " + uriStr);
+  catch (e) {
+    if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+      throw new Error("malformed URI: " + uriStr);
+    }
+    if (e.result == Cr.NS_ERROR_FAILURE ||
+        e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+      throw new Error("invalid URI: " + uriStr);
+    }
   }
 }
 
 function resolveResourceURI(uri) {
   var resolved;
   try {
     resolved = resProt.resolveURI(uri);
-  } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
-    throw new Error("resource does not exist: " + uri.spec);
-  };
+  }
+  catch (e) {
+    if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+      throw new Error("resource does not exist: " + uri.spec);
+    }
+  }
   return resolved;
 }
 
 let fromFilename = exports.fromFilename = function fromFilename(path) {
   var file = Cc['@mozilla.org/file/local;1']
              .createInstance(Ci.nsILocalFile);
   file.initWithPath(path);
   return ios.newFileURI(file).spec;
@@ -58,18 +63,21 @@ let toFilename = exports.toFilename = fu
   var uri = newURI(url);
   if (uri.scheme == "resource")
     uri = newURI(resolveResourceURI(uri));
   if (uri.scheme == "chrome") {
     var channel = ios.newChannelFromURI(uri);
     try {
       channel = channel.QueryInterface(Ci.nsIFileChannel);
       return channel.file.path;
-    } catch (e if e.result == Cr.NS_NOINTERFACE) {
-      throw new Error("chrome url isn't on filesystem: " + url);
+    }
+    catch (e) {
+      if (e.result == Cr.NS_NOINTERFACE) {
+        throw new Error("chrome url isn't on filesystem: " + url);
+      }
     }
   }
   if (uri.scheme == "file") {
     var file = uri.QueryInterface(Ci.nsIFileURL).file;
     return file.path;
   }
   throw new Error("cannot map to filename: " + url);
 };
@@ -79,27 +87,42 @@ function URL(url, base) {
      return new URL(url, base);
   }
 
   var uri = newURI(url, base);
 
   var userPass = null;
   try {
     userPass = uri.userPass ? uri.userPass : null;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   var host = null;
   try {
     host = uri.host;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   var port = null;
   try {
     port = uri.port == -1 ? null : uri.port;
-  } catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_FAILURE) {
+      throw e;
+    }
+  }
 
   let uriData = [uri.path, uri.path.length, {}, {}, {}, {}, {}, {}];
   URLParser.parsePath.apply(URLParser, uriData);
   let [{ value: filepathPos }, { value: filepathLen },
     { value: queryPos }, { value: queryLen },
     { value: refPos }, { value: refLen }] = uriData.slice(2);
 
   let hash = uri.ref ? "#" + uri.ref : "";
@@ -257,26 +280,31 @@ const DataURL = Class({
 
 exports.DataURL = DataURL;
 
 let getTLD = exports.getTLD = function getTLD (url) {
   let uri = newURI(url.toString());
   let tld = null;
   try {
     tld = tlds.getPublicSuffix(uri);
-  } catch (e if
-      e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS ||
-      e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {}
+  }
+  catch (e) {
+    if (e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS &&
+        e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+      throw e;
+    }
+  }
   return tld;
 };
 
 let isValidURI = exports.isValidURI = function (uri) {
   try {
     newURI(uri);
-  } catch(e) {
+  }
+  catch(e) {
     return false;
   }
   return true;
 }
 
 function isLocalURL(url) {
   if (String.indexOf(url, './') === 0)
     return true;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/util/dispatcher.js
@@ -0,0 +1,55 @@
+/* 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";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const method = require("method/core");
+
+// Utility function that is just an enhancement over `method` to
+// allow predicate based dispatch in addition to polymorphic
+// dispatch. Unfortunately polymorphic dispatch does not quite
+// cuts it in the world of XPCOM where no types / classes exist
+// and all the XUL nodes share same type / prototype.
+// Probably this is more generic and belongs some place else, but
+// we can move it later once this will be relevant.
+let dispatcher = hint => {
+  const base = method(hint);
+  // Make a map for storing predicate, implementation mappings.
+  let implementations = new Map();
+
+  // Dispatcher function goes through `predicate, implementation`
+  // pairs to find predicate that matches first argument and
+  // returns application of arguments on the associated
+  // `implementation`. If no matching predicate is found delegates
+  // to a `base` polymorphic function.
+  let dispatch = (value, ...rest) => {
+    for (let [predicate, implementation] of implementations) {
+      if (predicate(value))
+        return implementation(value, ...rest);
+    }
+
+    return base(value, ...rest);
+  };
+
+  // Expose base API.
+  dispatch.define = base.define;
+  dispatch.implement = base.implement;
+  dispatch.toString = base.toString;
+
+  // Add a `when` function to allow extending function via
+  // predicates.
+  dispatch.when = (predicate, implementation) => {
+    if (implementations.has(predicate))
+      throw TypeError("Already implemented for the given predicate");
+    implementations.set(predicate, implementation);
+  };
+
+  return dispatch;
+};
+
+exports.dispatcher = dispatcher;
--- a/addon-sdk/source/lib/sdk/windows.js
+++ b/addon-sdk/source/lib/sdk/windows.js
@@ -6,14 +6,31 @@
 module.metadata = {
   'stability': 'stable',
   'engines': {
     'Firefox': '*',
     'Fennec': '*'
   }
 };
 
+const { isBrowser } = require('./window/utils');
+const { modelFor } = require('./model/core');
+const { viewFor } = require('./view/core');
+
+
 if (require('./system/xul-app').is('Fennec')) {
   module.exports = require('./windows/fennec');
 }
 else {
   module.exports = require('./windows/firefox');
 }
+
+
+const browsers = module.exports.browserWindows;
+
+//
+modelFor.when(isBrowser, view => {
+  for (let model of browsers) {
+    if (viewFor(model) === view)
+      return model;
+  }
+  return null;
+});
--- a/addon-sdk/source/test/addons/places/tests/test-places-favicon.js
+++ b/addon-sdk/source/test/addons/places/tests/test-places-favicon.js
@@ -142,23 +142,23 @@ exports.testTabsGetFaviconPromiseFailure
     onOpen: function (newTab) tab = newTab,
     inBackground: true
   });
 };
 
 exports.testRejects = function (assert, done) {
   getFavicon({})
     .then(invalidResolve(assert), validReject(assert, 'Object'))
-  .then(getFavicon(null))
+  .then(() => getFavicon(null))
     .then(invalidResolve(assert), validReject(assert, 'null'))
-  .then(getFavicon(undefined))
+  .then(() => getFavicon(undefined))
     .then(invalidResolve(assert), validReject(assert, 'undefined'))
-  .then(getFavicon([]))
+  .then(() => getFavicon([]))
     .then(invalidResolve(assert), validReject(assert, 'Array'))
-    .then(done);
+    .catch(assert.fail).then(done);
 };
 
 function invalidResolve (assert) {
   return function () assert.fail('Promise should not be resolved successfully');
 }
 
 function validReject (assert, name) {
   return function () assert.pass(name + ' correctly rejected');
--- a/addon-sdk/source/test/addons/places/tests/test-places-utils.js
+++ b/addon-sdk/source/test/addons/places/tests/test-places-utils.js
@@ -26,36 +26,35 @@ exports['test construct tree'] = functio
   assert.equal(tree.get(4.32).value, 4.32, 'get returns node even if created from nested node');
   assert.equal(tree.get(4).children.length, 3, 'nodes have correct children length');
   assert.equal(tree.get(3).children.length, 0, 'nodes have correct children length');
 
   assert.equal(tree.get(4).get(4.32).value, 4.32, 'node.get descends from itself');
   assert.equal(tree.get(4).get(2), null, 'node.get descends from itself fails if not descendant');
 };
 
-exports['test walk'] = function (assert) {
+exports['test walk'] = function (assert, done) {
   let resultsAll = [];
+  let resultsNode = [];
   let tree = TreeNode(1);
   tree.add([2, 3, 4]);
   tree.get(2).add([2.1, 2.2]);
 
   tree.walk(function (node) {
     resultsAll.push(node.value);
-  });
-
-  [1, 2, 2.1, 2.2, 3, 4].forEach(function (num) {
-    assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
-  });
-
-  let resultsNode = [];
-  tree.get(2).walk(function (node) resultsNode.push(node.value));
-
-  [2, 2.1, 2.2].forEach(function (num) {
-    assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node');
-  });
+  }).then(() => {
+    [1, 2, 2.1, 2.2, 3, 4].forEach(num => {
+      assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
+    });
+    return tree.get(2).walk(node => resultsNode.push(node.value));
+  }).then(() => {
+    [2, 2.1, 2.2].forEach(function (num) {
+      assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node');
+    });
+  }).catch(assert.fail).then(done);
 };
 
 exports['test async walk'] = function (assert, done) {
   let resultsAll = [];
   let tree = TreeNode(1);
   tree.add([2, 3, 4]);
   tree.get(2).add([2.1, 2.2]);
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-dispatcher.js
@@ -0,0 +1,77 @@
+/* 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";
+
+const { dispatcher } = require("sdk/util/dispatcher");
+
+exports["test dispatcher API"] = assert => {
+  const dispatch = dispatcher();
+
+  assert.equal(typeof(dispatch), "function",
+               "dispatch is a function");
+
+  assert.equal(typeof(dispatch.define), "function",
+               "dispatch.define is a function");
+
+  assert.equal(typeof(dispatch.implement), "function",
+               "dispatch.implement is a function");
+
+  assert.equal(typeof(dispatch.when), "function",
+               "dispatch.when is a function");
+};
+
+exports["test dispatcher"] = assert => {
+  const isDuck = dispatcher();
+
+  const quacks = x => x && typeof(x.quack) === "function";
+
+  const Duck = function() {};
+  const Goose = function() {};
+
+  const True = _ => true;
+  const False = _ => false;
+
+
+
+  isDuck.define(Goose, False);
+  isDuck.define(Duck, True);
+  isDuck.when(quacks, True);
+
+  assert.equal(isDuck(new Goose()), false,
+               "Goose ain't duck");
+
+  assert.equal(isDuck(new Duck()), true,
+               "Ducks are ducks");
+
+  assert.equal(isDuck({ quack: () => "Quaaaaaack!" }), true,
+               "It's a duck if it quacks");
+
+
+  assert.throws(() => isDuck({}), /Type does not implements method/, "not implemneted");
+
+  isDuck.define(Object, False);
+
+  assert.equal(isDuck({}), false,
+               "Ain't duck if it does not quacks!");
+};
+
+exports["test redefining fails"] = assert => {
+  const isPM = dispatcher();
+  const isAfternoon = time => time.getHours() > 12;
+
+  isPM.when(isAfternoon, _ => true);
+
+  assert.equal(isPM(new Date(Date.parse("Jan 23, 1985, 13:20:00"))), true,
+               "yeap afternoon");
+  assert.equal(isPM({ getHours: _ => 17 }), true,
+                "seems like afternoon");
+
+  assert.throws(() => isPM.when(isAfternoon, x => x > 12 && x < 24),
+                /Already implemented for the given predicate/,
+               "can't redefine on same predicate");
+
+};
+
+require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-net-url.js
+++ b/addon-sdk/source/test/test-net-url.js
@@ -24,28 +24,16 @@ exports["test async readURI"] = function
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI"] = function(assert) {
-  let content = "";
-
-  readURI(data.url("test-net-url.txt"), { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, utf8text, "The URL content is loaded properly");
-}
-
 exports["test readURISync"] = function(assert) {
   let content = readURISync(data.url("test-net-url.txt"));
 
   assert.equal(content, utf8text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with ISO-8859-1 charset"] = function(assert, done) {
   let content = "";
@@ -57,55 +45,32 @@ exports["test async readURI with ISO-885
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with ISO-8859-1 charset"] = function(assert) {
-  let content = "";
-
-  readURI(data.url("test-net-url.txt"), {
-    sync: true,
-    charset: "ISO-8859-1"
-  }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, latin1text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with ISO-8859-1 charset"] = function(assert) {
   let content = readURISync(data.url("test-net-url.txt"), "ISO-8859-1");
 
   assert.equal(content, latin1text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with not existing file"] = function(assert, done) {
   readURI(data.url("test-net-url-fake.txt")).then(function(data) {
     assert.fail("should not resolve");
     done();
   }, function(reason) {
     assert.ok(reason.indexOf("Failed to read:") === 0);
     done();
   })
 }
 
-exports["test sync readURI with not existing file"] = function(assert) {
-  readURI(data.url("test-net-url-fake.txt"), { sync: true }).then(function(data) {
-    assert.fail("should not resolve");
-  }, function(reason) {
-    assert.ok(reason.indexOf("Failed to read:") === 0);
-  })
-}
-
 exports["test readURISync with not existing file"] = function(assert) {
   assert.throws(function() {
     readURISync(data.url("test-net-url-fake.txt"));
   }, /NS_ERROR_FILE_NOT_FOUND/);
 }
 
 exports["test async readURI with data URI"] = function(assert, done) {
   let content = "";
@@ -117,28 +82,16 @@ exports["test async readURI with data UR
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with data URI"] = function(assert) {
-  let content = "";
-
-  readURI(dataURIutf8, { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, utf8text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with data URI"] = function(assert) {
   let content = readURISync(dataURIutf8);
 
   assert.equal(content, utf8text, "The URL content is loaded properly");
 }
 
 exports["test async readURI with data URI and ISO-8859-1 charset"] = function(assert, done) {
   let content = "";
@@ -150,31 +103,16 @@ exports["test async readURI with data UR
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with data URI and ISO-8859-1 charset"] = function(assert) {
-  let content = "";
-
-  readURI(dataURIlatin1, {
-    sync: true,
-    charset: "ISO-8859-1"
-  }).then(function(data) {
-    content = unescape(data);
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, latin1text, "The URL content is loaded properly");
-}
-
 exports["test readURISync with data URI and ISO-8859-1 charset"] = function(assert) {
   let content = unescape(readURISync(dataURIlatin1, "ISO-8859-1"));
 
   assert.equal(content, latin1text, "The URL content is loaded properly");
 }
 
 exports["test readURISync with chrome URI"] = function(assert) {
   let content = readURISync(chromeURI);
@@ -192,21 +130,9 @@ exports["test async readURI with chrome 
   }, function() {
     assert.fail("should not reject");
     done();
   })
 
   assert.equal(content, "", "The URL content is not load yet");
 }
 
-exports["test sync readURI with chrome URI"] = function(assert) {
-  let content = "";
-
-  readURI(chromeURI, { sync: true }).then(function(data) {
-    content = data;
-  }, function() {
-    assert.fail("should not reject");
-  })
-
-  assert.equal(content, readURISync(chromeURI), "The URL content is loaded properly");
-}
-
 require("test").run(exports)
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -19,16 +19,17 @@ const { getTabContentWindow, getActiveTa
 const xulApp = require("sdk/system/xul-app");
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { isPrivate } = require('sdk/private-browsing');
 const { openWebpage } = require('./private-browsing/helper');
 const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
 const promise = require("sdk/core/promise");
 const { pb } = require('./private-browsing/helper');
 const { URL } = require("sdk/url");
+const { LoaderWithHookedConsole } = require('sdk/test/loader');
 
 const { waitUntil } = require("sdk/test/utils");
 const data = require("./fixtures");
 
 const { gDevToolsExtensions } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
 
 const testPageURI = data.url("test.html");
 
@@ -1526,16 +1527,51 @@ exports.testDetachOnUnload = function(as
   });
 
   tabs.open({
     url: TEST_URL,
     onOpen: t => tab = t
   })
 }
 
+exports.testConsole = function(assert, done) {
+  let innerID;
+  const TEST_URL = 'data:text/html;charset=utf-8,console';
+  const { loader } = LoaderWithHookedConsole(module, onMessage);
+  const { PageMod } = loader.require('sdk/page-mod');
+  const system = require("sdk/system/events");
+
+  let seenMessage = false;
+  function onMessage(type, msg, msgID) {
+    seenMessage = true;
+    innerID = msgID;
+  }
+
+  let mod = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: "ready",
+    contentScript: Isolate(function() {
+      console.log("Hello from the page mod");
+      self.port.emit("done");
+    }),
+    onAttach: function(worker) {
+      worker.port.on("done", function() {
+        let window = getTabContentWindow(tab);
+        let id = getInnerId(window);
+        assert.ok(seenMessage, "Should have seen the console message");
+        assert.equal(innerID, id, "Should have seen the right inner ID");
+        closeTab(tab);
+        done();
+      });
+    },
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+}
+
 exports.testSyntaxErrorInContentScript = function(assert, done) {
   const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript";
   let hitError = null;
   let attached = false;
 
   testPageMod(assert, done, url, [{
       include: url,
       contentScript: 'console.log(23',
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -449,15 +449,20 @@ exports.testMessageQueue = function (ass
     done();
   });
 };
 
 function isDestroyed(page) {
   try {
     page.postMessage("foo");
   }
-  catch (err if err.message == ERR_DESTROYED) {
-    return true;
+  catch (err) {
+    if (err.message == ERR_DESTROYED) {
+      return true;
+    }
+    else {
+      throw err;
+    }
   }
   return false;
 }
 
 require("test").run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -896,17 +896,17 @@ exports['test passing DOM node as first 
   let widget = Widget({
     id: 'panel-widget',
     label: 'panel widget',
     content: '<i></i>',
   });
 
   let widgetNode = document.getElementById(widgetId);
 
-  all(warned.promise, shown.promise).
+  all([warned.promise, shown.promise]).
     then(loader.unload).
     then(done, assert.fail)
 
   panel.show(widgetNode);
 };
 
 // This test is checking that `onpupshowing` events emitted by panel's children
 // are not considered.
--- a/addon-sdk/source/test/test-plain-text-console.js
+++ b/addon-sdk/source/test/test-plain-text-console.js
@@ -225,16 +225,41 @@ exports.testPlainTextConsoleBoundMethods
   let tbLines = prints[0].split("\n");
   assert.equal(tbLines[0], "console.trace: " + name + ": ");
   assert.ok(tbLines[1].indexOf("_ain-text-console.js 224") === 0);
   prints = [];
 
   restorePrefs();
 };
 
+exports.testConsoleInnerID = function(assert) {
+  let Console = require("sdk/console/plain-text").PlainTextConsole;
+  let { log, info, warn, error, debug, exception, trace } = new Console(function() {}, "test ID");
+
+  let messages = [];
+  function onMessage({ subject }) {
+    let message = subject.wrappedJSObject;
+    messages.push({ msg: message.arguments[0], type: message.level, innerID: message.innerID });
+  }
+
+  const system = require("sdk/system/events");
+  system.on("console-api-log-event", onMessage);
+
+  log("Test log");
+  warn("Test warning");
+  error("Test error");
+
+  assert.equal(messages.length, 3, "Should see 3 log events");
+  assert.deepEqual(messages[0], { msg: "Test log", type: "log", innerID: "test ID" }, "Should see the right event");
+  assert.deepEqual(messages[1], { msg: "Test warning", type: "warn", innerID: "test ID" }, "Should see the right event");
+  assert.deepEqual(messages[2], { msg: "Test error", type: "error", innerID: "test ID" }, "Should see the right event");
+
+  system.off("console-api-log-event", onMessage);
+};
+
 function restorePrefs() {
   if (HAS_ORIGINAL_ADDON_LOG_LEVEL)
     prefs.set(ADDON_LOG_LEVEL_PREF, ORIGINAL_ADDON_LOG_LEVEL);
   else
     prefs.reset(ADDON_LOG_LEVEL_PREF);
 
   if (HAS_ORIGINAL_SDK_LOG_LEVEL)
     prefs.set(SDK_LOG_LEVEL_PREF, ORIGINAL_SDK_LOG_LEVEL);
--- a/addon-sdk/source/test/test-promise.js
+++ b/addon-sdk/source/test/test-promise.js
@@ -1,349 +1,318 @@
 /* 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';
 
-var core = require('sdk/core/promise'),
-    defer = core.defer, resolve = core.resolve, reject = core.reject, all = core.all,
-    promised = core.promised;
-
-var { setTimeout } = require('sdk/timers');
+const { Cc, Cu, Ci } = require('chrome');
+const { setTimeout } = require('sdk/timers');
+const { prefixURI, name } = require('@loader/options');
+const addonPromiseURI = prefixURI + name + '/lib/sdk/core/promise.js';
+const builtPromiseURI = 'resource://gre/modules/commonjs/sdk/core/promise.js';
+let { Promise, defer, resolve, reject, all, promised } = require('sdk/core/promise');
 
 exports['test all observers are notified'] = function(assert, done) {
-  var expected = 'Taram pam param!'
-  var deferred = defer()
-  var pending = 10, i = 0
+  let expected = 'Taram pam param!';
+  let deferred = defer();
+  let pending = 10, i = 0;
 
   function resolved(value) {
-    assert.equal(value, expected, 'value resolved as expected: #' + pending)
-    if (!--pending) done()
+    assert.equal(value, expected, 'value resolved as expected: #' + pending);
+    if (!--pending) done();
   }
 
-  while (i++ < pending) deferred.promise.then(resolved)
+  while (i++ < pending) deferred.promise.then(resolved);
 
-  deferred.resolve(expected)
-}
+  deferred.resolve(expected);
+};
 
 exports['test exceptions dont stop notifications'] = function(assert, done) {
-  var threw = false, boom = Error('Boom!')
-  var deferred = defer()
+  let threw = false, boom = Error('Boom!');
+  let deferred = defer();
 
-  var promise2 = deferred.promise.then(function() {
-    threw = true
-    throw boom
-  })
+  let promise2 = deferred.promise.then(function() {
+    threw = true;
+    throw boom;
+  });
 
   deferred.promise.then(function() {
-    assert.ok(threw, 'observer is called even though previos one threw')
+    assert.ok(threw, 'observer is called even though previos one threw');
     promise2.then(function() {
-      assert.fail('should not resolve')
+      assert.fail('should not resolve');
     }, function(reason) {
-      assert.equal(reason, boom, 'rejects to thrown error')
-      done()
-    })
-  })
+      assert.equal(reason, boom, 'rejects to thrown error');
+      done();
+    });
+  });
 
-  deferred.resolve('go!')
-}
+  deferred.resolve('go!');
+};
 
 exports['test subsequent resolves are ignored'] = function(assert, done) {
-  var deferred = defer()
-  deferred.resolve(1)
-  deferred.resolve(2)
-  deferred.reject(3)
+  let deferred = defer();
+  deferred.resolve(1);
+  deferred.resolve(2);
+  deferred.reject(3);
 
   deferred.promise.then(function(actual) {
-    assert.equal(actual, 1, 'resolves to first value')
+    assert.equal(actual, 1, 'resolves to first value');
   }, function() {
-    assert.fail('must not reject')
-  })
+    assert.fail('must not reject');
+  });
   deferred.promise.then(function(actual) {
-    assert.equal(actual, 1, 'subsequent resolutions are ignored')
-    done()
+    assert.equal(actual, 1, 'subsequent resolutions are ignored');
+    done();
   }, function() {
-    assert.fail('must not reject')
-  })
-}
+    assert.fail('must not reject');
+  });
+};
 
 exports['test subsequent rejections are ignored'] = function(assert, done) {
-  var deferred = defer()
-  deferred.reject(1)
-  deferred.resolve(2)
-  deferred.reject(3)
+  let deferred = defer();
+  deferred.reject(1);
+  deferred.resolve(2);
+  deferred.reject(3);
 
   deferred.promise.then(function(actual) {
-    assert.fail('must not resolve')
+    assert.fail('must not resolve');
   }, function(actual) {
-    assert.equal(actual, 1, 'must reject to first')
-  })
+    assert.equal(actual, 1, 'must reject to first');
+  });
   deferred.promise.then(function(actual) {
-    assert.fail('must not resolve')
+    assert.fail('must not resolve');
   }, function(actual) {
-    assert.equal(actual, 1, 'must reject to first')
-    done()
-  })
-}
+    assert.equal(actual, 1, 'must reject to first');
+    done();
+  });
+};
 
 exports['test error recovery'] = function(assert, done) {
-  var boom = Error('Boom!')
-  var deferred = defer()
+  let boom = Error('Boom!');
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('rejected promise should not resolve')
+    assert.fail('rejected promise should not resolve');
   }, function(reason) {
-    assert.equal(reason, boom, 'rejection reason delivered')
-    return 'recovery'
+    assert.equal(reason, boom, 'rejection reason delivered');
+    return 'recovery';
   }).then(function(value) {
-    assert.equal(value, 'recovery', 'error handled by a handler')
-    done()
-  })
+    assert.equal(value, 'recovery', 'error handled by a handler');
+    done();
+  });
 
-  deferred.reject(boom)
-}
-
+  deferred.reject(boom);
+};
 
 exports['test error recovery with promise'] = function(assert, done) {
-  var deferred = defer()
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('must reject')
+    assert.fail('must reject');
   }, function(actual) {
-    assert.equal(actual, 'reason', 'rejected')
-    var deferred = defer()
-    deferred.resolve('recovery')
-    return deferred.promise
+    assert.equal(actual, 'reason', 'rejected');
+    let deferred = defer();
+    deferred.resolve('recovery');
+    return deferred.promise;
   }).then(function(actual) {
-    assert.equal(actual, 'recovery', 'recorvered via promise')
-    var deferred = defer()
-    deferred.reject('error')
-    return deferred.promise
+    assert.equal(actual, 'recovery', 'recorvered via promise');
+    let deferred = defer();
+    deferred.reject('error');
+    return deferred.promise;
   }).then(null, function(actual) {
-    assert.equal(actual, 'error', 'rejected via promise')
-    var deferred = defer()
-    deferred.reject('end')
-    return deferred.promise
+    assert.equal(actual, 'error', 'rejected via promise');
+    let deferred = defer();
+    deferred.reject('end');
+    return deferred.promise;
   }).then(null, function(actual) {
-    assert.equal(actual, 'end', 'rejeced via promise')
-    done()
-  })
+    assert.equal(actual, 'end', 'rejeced via promise');
+    done();
+  });
 
-  deferred.reject('reason')
-}
+  deferred.reject('reason');
+};
 
 exports['test propagation'] = function(assert, done) {
-  var d1 = defer(), d2 = defer(), d3 = defer()
+  let d1 = defer(), d2 = defer(), d3 = defer();
 
   d1.promise.then(function(actual) {
-    assert.equal(actual, 'expected', 'resolves to expected value')
-    done()
-  })
+    assert.equal(actual, 'expected', 'resolves to expected value');
+    done();
+  });
 
-  d1.resolve(d2.promise)
-  d2.resolve(d3.promise)
-  d3.resolve('expected')
-}
+  d1.resolve(d2.promise);
+  d2.resolve(d3.promise);
+  d3.resolve('expected');
+};
 
 exports['test chaining'] = function(assert, done) {
-  var boom = Error('boom'), brax = Error('braxXXx')
-  var deferred = defer()
+  let boom = Error('boom'), brax = Error('braxXXx');
+  let deferred = defer();
 
   deferred.promise.then().then().then(function(actual) {
-    assert.equal(actual, 2, 'value propagates unchanged')
-    return actual + 2
+    assert.equal(actual, 2, 'value propagates unchanged');
+    return actual + 2;
   }).then(null, function(reason) {
-    assert.fail('should not reject')
+    assert.fail('should not reject');
   }).then(function(actual) {
-    assert.equal(actual, 4, 'value propagates through if not handled')
-    throw boom
+    assert.equal(actual, 4, 'value propagates through if not handled');
+    throw boom;
   }).then(function(actual) {
-    assert.fail('exception must reject promise')
+    assert.fail('exception must reject promise');
   }).then().then(null, function(actual) {
-    assert.equal(actual, boom, 'reason propagates unchanged')
-    throw brax
+    assert.equal(actual, boom, 'reason propagates unchanged');
+    throw brax;
   }).then().then(null, function(actual) {
-    assert.equal(actual, brax, 'reason changed becase of exception')
-    return 'recovery'
+    assert.equal(actual, brax, 'reason changed becase of exception');
+    return 'recovery';
   }).then(function(actual) {
-    assert.equal(actual, 'recovery', 'recovered from error')
-    done()
-  })
+    assert.equal(actual, 'recovery', 'recovered from error');
+    done();
+  });
 
-  deferred.resolve(2)
-}
-
+  deferred.resolve(2);
+};
 
 exports['test reject'] = function(assert, done) {
-  var expected = Error('boom')
+  let expected = Error('boom');
 
   reject(expected).then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected with expected reason')
-  }).then(function() {
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejected with expected reason');
+  }).then(done, assert.fail);
+};
 
 exports['test resolve to rejected'] = function(assert, done) {
-  var expected = Error('boom')
-  var deferred = defer()
+  let expected = Error('boom');
+  let deferred = defer();
 
   deferred.promise.then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected with expected failure')
-  }).then(function() {
-    done()
-  })
+    assert.equal(actual, expected, 'rejected with expected failure');
+  }).then(done, assert.fail);
 
-  deferred.resolve(reject(expected))
-}
+  deferred.resolve(reject(expected));
+};
 
 exports['test resolve'] = function(assert, done) {
-  var expected = 'value'
+  let expected = 'value';
   resolve(expected).then(function(actual) {
-    assert.equal(actual, expected, 'resolved as expected')
-  }).then(function() {
-    done()
-  })
-}
-
-exports['test resolve with prototype'] = function(assert, done) {
-  var seventy = resolve(70, {
-    subtract: function subtract(y) {
-      return this.then(function(x) { return x - y })
-    }
-  })
-
-  seventy.subtract(17).then(function(actual) {
-    assert.equal(actual, 70 - 17, 'resolves to expected')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'resolved as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised with normal args'] = function(assert, done) {
-  var sum = promised(function(x, y) { return x + y })
+  let sum = promised((x, y) => x + y );
 
   sum(7, 8).then(function(actual) {
-    assert.equal(actual, 7 + 8, 'resolves as expected')
-    done()
-  })
-}
+    assert.equal(actual, 7 + 8, 'resolves as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised with promise args'] = function(assert, done) {
-  var sum = promised(function(x, y) { return x + y })
-  var deferred = defer()
+  let sum = promised((x, y) => x + y );
+  let deferred = defer();
 
   sum(11, deferred.promise).then(function(actual) {
-    assert.equal(actual, 11 + 24, 'resolved as expected')
-    done()
-  })
-
-  deferred.resolve(24)
-}
-
-exports['test promised with prototype'] = function(assert, done) {
-  var deferred = defer()
-  var numeric = {}
-  numeric.subtract = promised(function(y) { return this - y }, numeric)
+    assert.equal(actual, 11 + 24, 'resolved as expected');
+  }).catch(assert.fail).then(done);
 
-  var sum = promised(function(x, y) { return x + y }, numeric)
-
-  sum(7, 70).
-    subtract(14).
-    subtract(deferred.promise).
-    subtract(5).
-    then(function(actual) {
-      assert.equal(actual, 7 + 70 - 14 - 23 - 5, 'resolved as expected')
-      done()
-    })
-
-  deferred.resolve(23)
-}
+  deferred.resolve(24);
+};
 
 exports['test promised error handleing'] = function(assert, done) {
-  var expected = Error('boom')
-  var f = promised(function() {
-    throw expected
-  })
+  let expected = Error('boom');
+  let f = promised(function() {
+    throw expected;
+  });
 
   f().then(function() {
-    assert.fail('should reject')
+    assert.fail('should reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejected as expected')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejected as expected');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test errors in promise resolution handlers are propagated'] = function(assert, done) {
   var expected = Error('Boom');
   var { promise, resolve } = defer();
 
   promise.then(function() {
     throw expected;
   }).then(function() {
     return undefined;
   }).then(null, function(actual) {
     assert.equal(actual, expected, 'rejected as expected');
   }).then(done, assert.fail);
 
   resolve({});
-}
+};
 
 exports['test return promise form promised'] = function(assert, done) {
-  var f = promised(function() {
-    return resolve(17)
-  })
+  let f = promised(function() {
+    return resolve(17);
+  });
 
   f().then(function(actual) {
-    assert.equal(actual, 17, 'resolves to a promise resolution')
-    done()
-  })
-}
+    assert.equal(actual, 17, 'resolves to a promise resolution');
+  }).catch(assert.fail).then(done);
+};
 
 exports['test promised returning failure'] = function(assert, done) {
-  var expected = Error('boom')
-  var f = promised(function() {
-    return reject(expected)
-  })
+  let expected = Error('boom');
+  let f = promised(function() {
+    return reject(expected);
+  });
 
   f().then(function() {
-    assert.fail('must reject')
+    assert.fail('must reject');
   }, function(actual) {
-    assert.equal(actual, expected, 'rejects with expected reason')
-    done()
-  })
-}
+    assert.equal(actual, expected, 'rejects with expected reason');
+  }).catch(assert.fail).then(done);
+};
 
-exports['test promised are greedy'] = function(assert, done) {
-  var runs = 0
-  var f = promised(function() { ++runs })
-  var promise = f()
-  assert.equal(runs, 1, 'promised runs task right away')
-  done()
-}
+/*
+ * Changed for compliance in Bug 881047, promises are now always async
+ */
+exports['test promises are always async'] = function (assert, done) {
+  let runs = 0;
+  resolve(1)
+    .then(val => ++runs)
+    .catch(assert.fail).then(done);
+  assert.equal(runs, 0, 'resolutions are called in following tick');
+};
+
+/*
+ * Changed for compliance in Bug 881047, promised's are now non greedy
+ */
+exports['test promised are not greedy'] = function(assert, done) {
+  let runs = 0;
+  promised(() => ++runs)()
+    .catch(assert.fail).then(done);
+  assert.equal(runs, 0, 'promised does not run task right away');
+};
 
 exports['test arrays should not flatten'] = function(assert, done) {
-  var a = defer()
-  var b = defer()
+  let a = defer();
+  let b = defer();
 
-  var combine = promised(function(str, arr) {
-    assert.equal(str, 'Hello', 'Array was not flattened')
-    assert.deepEqual(arr, [ 'my', 'friend' ])
-  })
+  let combine = promised(function(str, arr) {
+    assert.equal(str, 'Hello', 'Array was not flattened');
+    assert.deepEqual(arr, [ 'my', 'friend' ]);
+  });
 
-  combine(a.promise, b.promise).then(done)
+  combine(a.promise, b.promise).catch(assert.fail).then(done);
 
 
-  a.resolve('Hello')
-  b.resolve([ 'my', 'friend' ])
-}
+  a.resolve('Hello');
+  b.resolve([ 'my', 'friend' ]);
+};
 
 exports['test `all` for all promises'] = function (assert, done) {
   all([
     resolve(5), resolve(7), resolve(10)
   ]).then(function (val) {
     assert.equal(
       val[0] === 5 &&
       val[1] === 7 &&
@@ -361,17 +330,17 @@ exports['test `all` aborts upon first re
   ]).then(function (val) {
     assert.fail('Successful resolve function should not be called');
   }, function (reason) {
     assert.equal(reason, 'error', 'should reject the `all` promise');
     done();
   });
 
   function delayedResolve () {
-    var deferred = defer();
+    let deferred = defer();
     setTimeout(deferred.resolve, 50);
     return deferred.promise;
   }
 };
 
 exports['test `all` with array containing non-promise'] = function (assert, done) {
   all([
     resolve(5), resolve(10), 925
@@ -399,9 +368,83 @@ exports['test `all` with multiple reject
   ]).then(function (value) {
     assert.fail('should not be successful');
   }, function (reason) {
     assert.equal(reason, 'error1', 'should reject on first promise reject');
     done();
   });
 };
 
-require("test").run(exports)
+exports['test Promise constructor resolve'] = function (assert, done) {
+  var isAsync = true;
+  new Promise(function (resolve, reject) {
+    resolve(5);
+  }).then(x => {
+    isAsync = false;
+    assert.equal(x, 5, 'Promise constructor resolves correctly');
+  }).catch(assert.fail).then(done);
+  assert.ok(isAsync, 'Promise constructor runs async');
+};
+
+exports['test Promise constructor reject'] = function (assert, done) {
+  new Promise(function (resolve, reject) {
+    reject(new Error('deferred4life'));
+  }).then(assert.fail, (err) => {
+    assert.equal(err.message, 'deferred4life', 'Promise constructor rejects correctly');
+  }).catch(assert.fail).then(done);
+};
+
+exports['test JSM Load and API'] = function (assert, done) {
+  // Use addon URL when loading from cfx/local:
+  // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js
+  // Use built URL when testing on try, etc.
+  // resource://gre/modules/commonjs/sdk/core/promise.js
+  try {
+    var { Promise } = Cu.import(addonPromiseURI, {});
+  } catch (e) {
+    var { Promise } = Cu.import(builtPromiseURI, {});
+  }
+  testEnvironment(Promise, assert, done, 'JSM');
+};
+
+exports['test mozIJSSubScriptLoader exporting'] = function (assert, done) {
+  let { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
+  let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+  let Promise = new Cu.Sandbox(systemPrincipal);
+  let loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
+                 .getService(Ci.mozIJSSubScriptLoader);
+
+  // Use addon URL when loading from cfx/local:
+  // resource://90111c90-c31e-4dc7-ac35-b65947434435-at-jetpack/addon-sdk/lib/sdk/core/promise.js
+  // Use built URL when testing on try, etc.
+  // resource://gre/modules/commonjs/sdk/core/promise.js
+  try {
+    loader.loadSubScript(addonPromiseURI, Promise);
+  } catch (e) {
+    loader.loadSubScript(builtPromiseURI, Promise);
+  }
+
+  testEnvironment(Promise, assert, done, 'mozIJSSubScript');
+};
+
+function testEnvironment ({all, resolve, defer, reject, promised}, assert, done, type) {
+  all([resolve(5), resolve(10), 925]).then(val => {
+    assert.equal(val[0], 5, 'promise#all works ' + type);
+    assert.equal(val[1], 10, 'promise#all works ' + type);
+    assert.equal(val[2], 925, 'promise#all works ' + type);
+    return resolve(1000);
+  }).then(value => {
+    assert.equal(value, 1000, 'promise#resolve works ' + type);
+    return reject('testing reject');
+  }).then(null, reason => {
+    assert.equal(reason, 'testing reject', 'promise#reject works ' + type);
+    let deferred = defer();
+    setTimeout(() => deferred.resolve('\\m/'), 10);
+    return deferred.promise;
+  }).then(value => {
+    assert.equal(value, '\\m/', 'promise#defer works ' + type);
+    return promised(x => x * x)(5);
+  }).then(value => {
+    assert.equal(value, 25, 'promise#promised works ' + type);
+  }).then(done, assert.fail);
+}
+
+require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-simple-storage.js
+++ b/addon-sdk/source/test/test-simple-storage.js
@@ -1,49 +1,52 @@
 /* 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/. */
 
 const file = require("sdk/io/file");
 const prefs = require("sdk/preferences/service");
 
 const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
+const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
 
 let {Cc,Ci} = require("chrome");
 
 const { Loader } = require("sdk/test/loader");
 const { id } = require("sdk/self");
 
 let storeFile = Cc["@mozilla.org/file/directory_service;1"].
                 getService(Ci.nsIProperties).
                 get("ProfD", Ci.nsIFile);
 storeFile.append("jetpack");
 storeFile.append(id);
 storeFile.append("simple-storage");
+file.mkpath(storeFile.path);
 storeFile.append("store.json");
 let storeFilename = storeFile.path;
 
 function manager(loader) loader.sandbox("sdk/simple-storage").manager;
 
 exports.testSetGet = function (assert, done) {
   // Load the module once, set a value.
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function (storage) {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
+    assert.equal(ss.storage.foo, val, "Value should persist");
     manager(loader).jsonStore.onWrite = function (storage) {
-      file.remove(storeFilename);
-      done();
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.equal(ss.storage.foo, val, "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   let val = "foo";
   ss.storage.foo = val;
   assert.equal(ss.storage.foo, val, "Value read should be value set");
   loader.unload();
 };
 
 exports.testSetGetRootArray = function (assert, done) {
@@ -99,28 +102,46 @@ exports.testSetGetRootUndefined = functi
 
 exports.testEmpty = function (assert) {
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   loader.unload();
   assert.ok(!file.exists(storeFilename), "Store file should not exist");
 };
 
+exports.testStorageDataRecovery = function(assert) {
+  const data = { 
+    a: true,
+    b: [3, 13],
+    c: "guilty!",
+    d: { e: 1, f: 2 }
+  };
+  let stream = file.open(storeFilename, "w");
+  stream.write(JSON.stringify(data));
+  stream.close();
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  assert.deepEqual(ss.storage, data, "Recovered data should be the same as written");
+  file.remove(storeFilename);
+  loader.unload();
+}
+
 exports.testMalformed = function (assert) {
   let stream = file.open(storeFilename, "w");
   stream.write("i'm not json");
   stream.close();
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   let empty = true;
   for (let key in ss.storage) {
     empty = false;
     break;
   }
   assert.ok(empty, "Malformed storage should cause root to be empty");
+  file.remove(storeFilename);
   loader.unload();
 };
 
 // Go over quota and handle it by listener.
 exports.testQuotaExceededHandle = function (assert, done) {
   prefs.set(QUOTA_PREF, 18);
 
   let loader = Loader(module);
@@ -136,20 +157,21 @@ exports.testQuotaExceededHandle = functi
       let numProps = 0;
       for (let prop in ss.storage)
         numProps++;
       assert.ok(numProps, 2,
                   "Store should contain 2 values: " + ss.storage.toSource());
       assert.equal(ss.storage.x, 4, "x value should be correct");
       assert.equal(ss.storage.y, 5, "y value should be correct");
       manager(loader).jsonStore.onWrite = function (storage) {
-        prefs.reset(QUOTA_PREF);
-        done();
+        assert.fail("Nothing should be written since `storage` was not changed.");
       };
       loader.unload();
+      prefs.reset(QUOTA_PREF);
+      done();
     };
     loader.unload();
   });
   // This will be JSON.stringify()ed to: {"a":1,"b":2,"c":3} (19 bytes)
   ss.storage = { a: 1, b: 2, c: 3 };
   manager(loader).jsonStore.write();
 };
 
@@ -173,16 +195,19 @@ exports.testQuotaExceededNoHandle = func
       };
       loader.unload();
 
       loader = Loader(module);
       ss = loader.require("sdk/simple-storage");
       assert.equal(ss.storage, val,
                        "Over-quota value should not have been written, " +
                        "old value should have persisted: " + ss.storage);
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.fail("Nothing should be written since `storage` was not changed.");
+      };
       loader.unload();
       prefs.reset(QUOTA_PREF);
       done();
     });
     manager(loader).jsonStore.write();
   };
 
   let val = "foo";
@@ -227,16 +252,194 @@ exports.testUninstall = function (assert
     loader.unload("uninstall");
     assert.ok(!file.exists(storeFilename), "Store file should be removed");
     done();
   };
   ss.storage.foo = "foo";
   loader.unload();
 };
 
+exports.testChangeInnerArray = function(assert, done) {
+  prefs.set(WRITE_PERIOD_PREF, 10);
+
+  let expected = {
+    x: [5, 7],
+    y: [7, 28],
+    z: [6, 2]
+  };
+
+  // Load the module, set a value.
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  manager(loader).jsonStore.onWrite = function (storage) {
+    assert.ok(file.exists(storeFilename), "Store file should exist");
+
+    // Load the module again and check the result
+    loader = Loader(module);
+    ss = loader.require("sdk/simple-storage");
+    assert.equal(JSON.stringify(ss.storage),
+                     JSON.stringify(expected), "Should see the expected object");
+
+    // Add a property
+    ss.storage.x.push(["bar"]);
+    expected.x.push(["bar"]);
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.equal(JSON.stringify(ss.storage),
+                       JSON.stringify(expected), "Should see the expected object");
+
+      // Modify a property
+      ss.storage.y[0] = 42;
+      expected.y[0] = 42;
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.equal(JSON.stringify(ss.storage),
+                         JSON.stringify(expected), "Should see the expected object");
+
+        // Delete a property
+        delete ss.storage.z[1];
+        delete expected.z[1];
+        manager(loader).jsonStore.onWrite = function (storage) {
+          assert.equal(JSON.stringify(ss.storage),
+                           JSON.stringify(expected), "Should see the expected object");
+
+          // Modify the new inner-object
+          ss.storage.x[2][0] = "baz";
+          expected.x[2][0] = "baz";
+          manager(loader).jsonStore.onWrite = function (storage) {
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+
+            manager(loader).jsonStore.onWrite = function (storage) {
+              assert.fail("Nothing should be written since `storage` was not changed.");
+            };
+            loader.unload();
+
+            // Load the module again and check the result
+            loader = Loader(module);
+            ss = loader.require("sdk/simple-storage");
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+            loader.unload();
+            file.remove(storeFilename);
+            prefs.reset(WRITE_PERIOD_PREF);
+            done();
+          };
+        };
+      };
+    };
+  };
+
+  ss.storage = {
+    x: [5, 7],
+    y: [7, 28],
+    z: [6, 2]
+  };
+  assert.equal(JSON.stringify(ss.storage),
+                   JSON.stringify(expected), "Should see the expected object");
+
+  loader.unload();
+};
+
+exports.testChangeInnerObject = function(assert, done) {
+  prefs.set(WRITE_PERIOD_PREF, 10);
+
+  let expected = {
+    x: {
+      a: 5,
+      b: 7
+    },
+    y: {
+      c: 7,
+      d: 28
+    },
+    z: {
+      e: 6,
+      f: 2
+    }
+  };
+
+  // Load the module, set a value.
+  let loader = Loader(module);
+  let ss = loader.require("sdk/simple-storage");
+  manager(loader).jsonStore.onWrite = function (storage) {
+    assert.ok(file.exists(storeFilename), "Store file should exist");
+
+    // Load the module again and check the result
+    loader = Loader(module);
+    ss = loader.require("sdk/simple-storage");
+    assert.equal(JSON.stringify(ss.storage),
+                     JSON.stringify(expected), "Should see the expected object");
+
+    // Add a property
+    ss.storage.x.g = {foo: "bar"};
+    expected.x.g = {foo: "bar"};
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.equal(JSON.stringify(ss.storage),
+                       JSON.stringify(expected), "Should see the expected object");
+
+      // Modify a property
+      ss.storage.y.c = 42;
+      expected.y.c = 42;
+      manager(loader).jsonStore.onWrite = function (storage) {
+        assert.equal(JSON.stringify(ss.storage),
+                         JSON.stringify(expected), "Should see the expected object");
+
+        // Delete a property
+        delete ss.storage.z.f;
+        delete expected.z.f;
+        manager(loader).jsonStore.onWrite = function (storage) {
+          assert.equal(JSON.stringify(ss.storage),
+                           JSON.stringify(expected), "Should see the expected object");
+
+          // Modify the new inner-object
+          ss.storage.x.g.foo = "baz";
+          expected.x.g.foo = "baz";
+          manager(loader).jsonStore.onWrite = function (storage) {
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+
+            manager(loader).jsonStore.onWrite = function (storage) {
+              assert.fail("Nothing should be written since `storage` was not changed.");
+            };
+            loader.unload();
+
+            // Load the module again and check the result
+            loader = Loader(module);
+            ss = loader.require("sdk/simple-storage");
+            assert.equal(JSON.stringify(ss.storage),
+                             JSON.stringify(expected), "Should see the expected object");
+            loader.unload();
+            file.remove(storeFilename);
+            prefs.reset(WRITE_PERIOD_PREF);
+            done();
+          };
+        };
+      };
+    };
+  };
+
+  ss.storage = {
+    x: {
+      a: 5,
+      b: 7
+    },
+    y: {
+      c: 7,
+      d: 28
+    },
+    z: {
+      e: 6,
+      f: 2
+    }
+  };
+  assert.equal(JSON.stringify(ss.storage),
+                   JSON.stringify(expected), "Should see the expected object");
+
+  loader.unload();
+};
+
 exports.testSetNoSetRead = function (assert, done) {
   // Load the module, set a value.
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function (storage) {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again but don't access ss.storage.
@@ -245,22 +448,23 @@ exports.testSetNoSetRead = function (ass
     manager(loader).jsonStore.onWrite = function (storage) {
       assert.fail("Nothing should be written since `storage` was not accessed.");
     };
     loader.unload();
 
     // Load the module a third time and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
+    assert.equal(ss.storage.foo, val, "Value should persist");
     manager(loader).jsonStore.onWrite = function (storage) {
-      file.remove(storeFilename);
-      done();
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.equal(ss.storage.foo, val, "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   let val = "foo";
   ss.storage.foo = val;
   assert.equal(ss.storage.foo, val, "Value read should be value set");
   loader.unload();
 };
 
 
@@ -271,22 +475,23 @@ function setGetRoot(assert, done, val, c
   let loader = Loader(module);
   let ss = loader.require("sdk/simple-storage");
   manager(loader).jsonStore.onWrite = function () {
     assert.ok(file.exists(storeFilename), "Store file should exist");
 
     // Load the module again and make sure the value stuck.
     loader = Loader(module);
     ss = loader.require("sdk/simple-storage");
-    manager(loader).jsonStore.onWrite = function () {
-      file.remove(storeFilename);
-      done();
+    assert.ok(compare(ss.storage, val), "Value should persist");
+    manager(loader).jsonStore.onWrite = function (storage) {
+      assert.fail("Nothing should be written since `storage` was not changed.");
     };
-    assert.ok(compare(ss.storage, val), "Value should persist");
     loader.unload();
+    file.remove(storeFilename);
+    done();
   };
   ss.storage = val;
   assert.ok(compare(ss.storage, val), "Value read should be value set");
   loader.unload();
 }
 
 function setGetRootError(assert, done, val, msg) {
   let pred = new RegExp("storage must be one of the following types: " +
--- a/addon-sdk/source/test/test-tab.js
+++ b/addon-sdk/source/test/test-tab.js
@@ -1,19 +1,21 @@
 /* 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";
 
 const tabs = require("sdk/tabs"); // From addon-kit
 const windowUtils = require("sdk/deprecated/window-utils");
 const { getTabForWindow } = require('sdk/tabs/helpers');
 const app = require("sdk/system/xul-app");
 const { viewFor } = require("sdk/view/core");
-const { getTabId } = require("sdk/tabs/utils");
+const { modelFor } = require("sdk/model/core");
+const { getTabId, isTab } = require("sdk/tabs/utils");
 const { defer } = require("sdk/lang/functional");
 
 // The primary test tab
 var primaryTab;
 
 // We have an auxiliary tab to test background tabs.
 var auxTab;
 
@@ -147,11 +149,27 @@ exports["test viewFor(tab)"] = (assert, 
     const view = viewFor(tab);
     assert.ok(view, "view is returned");
     assert.equal(getTabId(view), tab.id, "tab has a same id");
 
     tab.close(defer(done));
   }));
 
   tabs.open({ url: "about:mozilla" });
-}
+};
+
+
+exports["test modelFor(xulTab)"] = (assert, done) => {
+  tabs.open({
+    url: "about:mozilla",
+    onReady: tab => {
+      const view = viewFor(tab);
+      assert.ok(view, "view is returned");
+      assert.ok(isTab(view), "view is underlaying tab");
+      assert.equal(getTabId(view), tab.id, "tab has a same id");
+      assert.equal(modelFor(view), tab, "modelFor(view) is SDK tab");
+
+      tab.close(defer(done));
+    }
+  });
+};
 
 require("test").run(exports);
--- a/addon-sdk/source/test/test-test-loader.js
+++ b/addon-sdk/source/test/test-test-loader.js
@@ -43,18 +43,18 @@ exports["test LoaderWithHookedConsole"] 
   let console = loader.globals.console;
   console.log("1st");
   console.error("2nd");
   console.warn("3rd");
   console.info("4th");
   console.debug("5th");
   console.exception("6th");
   assert.equal(messages.length, 6, "Got all console messages");
-  assert.deepEqual(messages[0], {type: "log", msg: "1st"}, "Got log");
-  assert.deepEqual(messages[1], {type: "error", msg: "2nd"}, "Got error");
-  assert.deepEqual(messages[2], {type: "warn", msg: "3rd"}, "Got warn");
-  assert.deepEqual(messages[3], {type: "info", msg: "4th"}, "Got info");
-  assert.deepEqual(messages[4], {type: "debug", msg: "5th"}, "Got debug");
-  assert.deepEqual(messages[5], {type: "exception", msg: "6th"}, "Got exception");
+  assert.deepEqual(messages[0], {type: "log", msg: "1st", innerID: null}, "Got log");
+  assert.deepEqual(messages[1], {type: "error", msg: "2nd", innerID: null}, "Got error");
+  assert.deepEqual(messages[2], {type: "warn", msg: "3rd", innerID: null}, "Got warn");
+  assert.deepEqual(messages[3], {type: "info", msg: "4th", innerID: null}, "Got info");
+  assert.deepEqual(messages[4], {type: "debug", msg: "5th", innerID: null}, "Got debug");
+  assert.deepEqual(messages[5], {type: "exception", msg: "6th", innerID: null}, "Got exception");
   assert.equal(count, 6, "Called for all messages");
 };
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-traceback.js
+++ b/addon-sdk/source/test/test-traceback.js
@@ -43,48 +43,63 @@ exports.testFormatDoesNotFetchRemoteFile
                        scheme + " request");
     });
 };
 
 exports.testFromExceptionWithString = function(assert) {
   try {
     throw "foob";
     assert.fail("an exception should've been thrown");
-  } catch (e if e == "foob") {
-    var tb = traceback.fromException(e);
-    assert.equal(tb.length, 0);
+  } catch (e) {
+    if (e == "foob") {
+      var tb = traceback.fromException(e);
+      assert.equal(tb.length, 0);
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFormatWithString = function(assert) {
   // This can happen if e.g. a thrown exception was
   // a string instead of an Error instance.
   assert.equal(traceback.format("blah"),
 		   "Traceback (most recent call last):");
 };
 
 exports.testFromExceptionWithError = function(assert) {
   try {
     throwError();
     assert.fail("an exception should've been thrown");
-  } catch (e if e instanceof Error) {
-    var tb = traceback.fromException(e);
+  } catch (e) {
+    if (e instanceof Error) {
+      var tb = traceback.fromException(e);
 
-    var xulApp = require("sdk/system/xul-app");
-    assert.equal(tb.slice(-1)[0].name, "throwError");
+      var xulApp = require("sdk/system/xul-app");
+      assert.equal(tb.slice(-1)[0].name, "throwError");
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFromExceptionWithNsIException = function(assert) {
   try {
     throwNsIException();
     assert.fail("an exception should've been thrown");
-  } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) {
-    var tb = traceback.fromException(e);
-    assert.equal(tb[tb.length - 1].name, "throwNsIException");
+  } catch (e) {
+    if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+      var tb = traceback.fromException(e);
+      assert.equal(tb[tb.length - 1].name, "throwNsIException");
+    }
+    else {
+      throw e;
+    }
   }
 };
 
 exports.testFormat = function(assert) {
   function getTraceback() {
     return traceback.format();
   }
 
--- a/addon-sdk/source/test/test-ui-frame.js
+++ b/addon-sdk/source/test/test-ui-frame.js
@@ -4,44 +4,61 @@
 "use strict";
 
 module.metadata = {
   "engines": {
     "Firefox": "*"
   }
 };
 
+const { Cu } = require("chrome");
 const { Frame } = require("sdk/ui/frame");
 const { Toolbar } = require("sdk/ui/toolbar");
 const { Loader } = require("sdk/test/loader");
 const { identify } = require("sdk/ui/id");
 const { setTimeout } = require("sdk/timers");
 const { getMostRecentBrowserWindow, open } = require("sdk/window/utils");
 const { ready, loaded, close } = require("sdk/window/helpers");
-const { defer } = require("sdk/core/promise");
+const { defer, all } = require("sdk/core/promise");
 const { send } = require("sdk/event/utils");
 const { object } = require("sdk/util/sequence");
 const { OutputPort } = require("sdk/output/system");
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const output = new OutputPort({ id: "toolbar-change" });
 
-const wait = (toolbar, event) => {
+const wait = (toolbar, event, times) => {
   let { promise, resolve } = defer();
-  toolbar.once(event, resolve);
+  if (times) {
+    let resolveArray = [];
+    let counter = 0;
+    toolbar.on(event, function onEvent (e) {
+      resolveArray.push(e);
+      if (++counter === times) {
+        toolbar.off(event, onEvent);
+        resolve(resolveArray);
+      }
+    });
+  }
+  else {
+    toolbar.once(event, resolve);
+  }
   return promise;
 };
 
 const stateEventsFor = frame =>
   [wait(frame, "attach"), wait(frame, "ready"),
    wait(frame, "load"), wait(frame, "detach")];
 
 
 const isAttached = ({id}, window=getMostRecentBrowserWindow()) =>
   !!window.document.getElementById(id);
 
-exports["test frame API"] = function*(assert) {
+// Use `Task.spawn` instead of `Task.async` because the returned function does not contain
+// a length for the test harness to determine whether the test should be executed
+exports["test frame API"] = function* (assert) {
   const url = "data:text/html,frame-api";
   assert.throws(() => new Frame(),
                 /The `options.url`/,
                 "must provide url");
 
   assert.throws(() => new Frame({ url: "http://mozilla.org/" }),
                 /The `options.url`/,
                 "options.url must be local url");
@@ -81,18 +98,17 @@ exports["test frame API"] = function*(as
   assert.pass("frame identical to destroyed one can be created");
 
   yield wait(f3, "register");
   assert.equal(f3.url, url, "url is set");
   f2.destroy();
   f3.destroy();
 };
 
-
-exports["test frame in toolbar"] = function*(assert) {
+exports["test frame in toolbar"] = function* (assert) {
   const assertEvent = (event, type) => {
     assert.ok(event, "`" + type + "` event was dispatched");
     assert.equal(event.type, type, "event.type is: " + type);
     assert.equal(typeof(event.source), "object",
                  "event.source is an object");
     assert.equal(typeof(event.source.postMessage), "function",
                  "messages can be posted to event.source");
   };
@@ -111,39 +127,37 @@ exports["test frame in toolbar"] = funct
   assertEvent((yield a1), "attach");
   assert.ok(isAttached(f1, w1), "frame is in the window#1");
   assertEvent((yield r1), "ready");
   assertEvent((yield l1), "load");
 
   const [a2, r2, l2] = stateEventsFor(f1);
   const w2 = open();
 
-
   assertEvent((yield a2), "attach");
   assert.ok(isAttached(f1, w2), "frame is in the window#2");
   assertEvent((yield r2), "ready");
   assertEvent((yield l2), "load");
   assert.pass("frame attached to window#2");
 
 
   const d1 = wait(f1, "detach");
   yield close(w2);
-
   assertEvent((yield d1), "detach");
   assert.pass("frame detached when window is closed");
 
   t1.destroy();
 
   assertEvent((yield wait(f1, "detach")), "detach");
   assert.ok(!isAttached(f1, w1), "frame was removed from window#1");
   assert.pass("toolbar destroy detaches frame");
 };
 
 
-exports["test host to content messaging"] = function*(assert) {
+exports["test host to content messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     window.addEventListener("message", (event) => {
       if (event.data === "ping!")
         event.source.postMessage("pong!", event.origin);
     });
   } + "</script>";
   const f1 = new Frame({ name: "mailbox", url: url });
   const t1 = new Toolbar({ title: "mailbox", items: [f1] });
@@ -154,17 +168,17 @@ exports["test host to content messaging"
   const pong = yield wait(f1, "message");
   assert.equal(pong.data, "pong!", "received ping back");
   t1.destroy();
 
   yield wait(t1, "detach");
 };
 
 
-exports["test content to host messaging"] = function*(assert) {
+exports["test content to host messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     window.addEventListener("message", (event) => {
       if (event.data === "pong!")
         event.source.postMessage("end", event.origin);
     });
 
     window.parent.postMessage("ping!", "*");
   } + "</script>";
@@ -177,20 +191,21 @@ exports["test content to host messaging"
 
   e1.source.postMessage("pong!", e1.origin);
 
   const e2 = yield wait(f1, "message");
   assert.equal(e2.data, "end", "received end message");
 
   t1.destroy();
   yield wait(t1, "detach");
+
 };
 
 
-exports["test direct messaging"] = function*(assert) {
+exports["test direct messaging"] = function* (assert) {
   const url = "data:text/html,<script>new " + function() {
     var n = 0;
     window.addEventListener("message", (event) => {
       if (event.data === "inc")
         n = n + 1;
       if (event.data === "print")
         event.source.postMessage({ n: n }, event.origin);
     });
@@ -203,36 +218,35 @@ exports["test direct messaging"] = funct
   yield wait(f1, "ready");
   assert.pass("document loaded in window#1");
 
   const w2 = open();
 
   yield wait(f1, "ready");
   assert.pass("document loaded in window#2");
 
+  let messages = wait(f1, "message", 2);
   f1.postMessage("inc", f1.origin);
   f1.postMessage("print", f1.origin);
 
-  const e1 = yield wait(f1, "message");
+  const [e1, e2] = yield messages;
   assert.deepEqual(e1.data, {n: 1}, "received message from window#1");
-
-  const e2 = yield wait(f1, "message");
   assert.deepEqual(e2.data, {n: 1}, "received message from window#2");
 
+  let message = wait(f1, "message");
   e1.source.postMessage("inc", e1.origin);
-
   e1.source.postMessage("print", e1.origin);
-
-  const e3 = yield wait(f1, "message");
+  const e3 = yield message;
   assert.deepEqual(e3.data, {n: 2}, "state changed in window#1");
 
+  let message = wait(f1, "message");
   e2.source.postMessage("print", e2.origin);
-  const e4 = yield wait(f1, "message");
-
+  yield message;
   assert.deepEqual(e2.data, {n:1}, "window#2 didn't received inc message");
 
   yield close(w2);
   t1.destroy();
 
   yield wait(t1, "detach");
+
 };
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-ui-sidebar.js
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -824,17 +824,17 @@ exports.testShowingInOneWindowDoesNotAff
       sidebar1.url += '1';
       assert.pass('set sidebar1.url');
     });
 
     sidebar1.show();
   }, assert.fail);
 }
 
-exports.testHidingAHiddenSidebarRejects = function(assert) {
+exports.testHidingAHiddenSidebarRejects = function(assert, done) {
   const { Sidebar } = require('sdk/ui/sidebar');
   let testName = 'testHidingAHiddenSidebarRejects';
   let url = 'data:text/html;charset=utf-8,'+testName;
   let sidebar = Sidebar({
     id: testName,
     title: testName,
     url: url
   });
@@ -1380,28 +1380,28 @@ exports.testEventListeners = function(as
     onReady.resolve();
   });
   sidebar.on('hide', function() {
     assert.equal(this, sidebar, '`this` is correct in on hide');
     eventListenerOrder.push('on hide');
     onHide.resolve();
   });
 
-  all(constructorOnShow.promise,
+  all([constructorOnShow.promise,
       constructorOnAttach.promise,
       constructorOnReady.promise,
       constructorOnHide.promise,
       onceShow.promise,
       onceAttach.promise,
       onceReady.promise,
       onceHide.promise,
       onShow.promise,
       onAttach.promise,
       onReady.promise,
-      onHide.promise).then(function() {
+      onHide.promise]).then(function() {
         assert.equal(eventListenerOrder.join(), [
             'onAttach',
             'once attach',
             'on attach',
             'onReady',
             'once ready',
             'on ready',
             'onShow',
@@ -1431,41 +1431,32 @@ exports.testAttachDoesNotEmitWhenShown =
       if (count > 2) {
         assert.fail('sidebar was attached again..');
       }
       else {
         assert.pass('sidebar was attached ' + count + ' time(s)');
       }
 
       if (++count == 1) {
-        setTimeout(function() {
-          let shown = false;
-          let endShownTest = false;
-          sidebar.once('show', function() {
-            assert.pass('shown was emitted');
-            shown = !endShownTest && true;
-          });
+        setImmediate(function() {
+          let shownFired = 0;
+          let onShow = () => shownFired++;
+          sidebar.on('show', onShow);
 
-          sidebar.show().then(function() {
-            assert.pass('calling hide');
-            sidebar.hide();
-          }).then(function() {
-            endShownTest = true;
-
-            setTimeout(function() {
-              sidebar.show().then(function() {
-                assert.ok(!shown, 'show did not emit');
-
-                sidebar.hide().then(function() {
-                  sidebar.destroy();
-                  done();
-                }).then(null, assert.fail);
-              })
-            })
-          }).then(null, assert.fail);
+          sidebar.show()
+          .then(() => assert.equal(shownFired, 0, 'shown should not be fired again when already showing from after attach'))
+          .then(sidebar.hide.bind(sidebar))
+          .then(sidebar.show.bind(sidebar))
+          .then(() => assert.equal(shownFired, 1, 'shown was emitted when `show` called after being hidden'))
+          .then(sidebar.show.bind(sidebar))
+          .then(() => {
+            assert.equal(shownFired, 1, 'shown was not emitted again if already being shown');
+            sidebar.off('show', onShow);
+            sidebar.destroy();
+          }).catch(assert.fail).then(done);
         });
       }
     }
   });
 
   sidebar.show();
 }
 
--- a/addon-sdk/source/test/test-windows-common.js
+++ b/addon-sdk/source/test/test-windows-common.js
@@ -1,16 +1,19 @@
 /* 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';
 
 const { Loader } = require('sdk/test/loader');
 const { browserWindows } = require('sdk/windows');
+const { viewFor } = require('sdk/view/core');
+const { modelFor } = require('sdk/model/core');
 const { Ci } = require("chrome");
+const { isBrowser, getWindowTitle } = require("sdk/window/utils");
 
 // TEST: browserWindows Iterator
 exports.testBrowserWindowsIterator = function(assert) {
   let activeWindowCount = 0;
   let windows = [];
   let i = 0;
   for each (let window in browserWindows) {
     if (window === browserWindows.activeWindow)
@@ -52,9 +55,42 @@ exports.testWindowActivateMethod_simple 
   window.activate();
 
   assert.equal(browserWindows.activeWindow, window,
                'Active window is active after window.activate() call');
   assert.equal(window.tabs.activeTab, tab,
                'Active tab is active after window.activate() call');
 };
 
+
+exports["test getView(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.equal(getWindowTitle(view), window.title,
+                 "window has a right title");
+
+    window.close(done);
+  });
+
+
+  browserWindows.open({ url: "data:text/html;charset=utf-8,<title>yo</title>" });
+};
+
+
+exports["test modelFor(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.ok(modelFor(view) === window, "modelFor(browserWindow) is SDK window");
+
+    window.close(done);
+  });
+
+
+  browserWindows.open({ url: "data:text/html;charset=utf-8,<title>yo</title>" });
+};
+
 require('sdk/test').run(exports);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -285,16 +285,21 @@ toolbarpaletteitem > #personal-bookmarks
 }
 
 toolbarpaletteitem > #personal-bookmarks > #bookmarks-toolbar-placeholder,
 #personal-bookmarks[cui-areatype="menu-panel"] > #bookmarks-toolbar-placeholder,
 #personal-bookmarks[cui-areatype="toolbar"][overflowedItem=true] > #bookmarks-toolbar-placeholder {
   display: -moz-box;
 }
 
+toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks,
+toolbar:not(#TabsToolbar) > #personal-bookmarks {
+  -moz-box-flex: 1;
+}
+
 #zoom-controls[cui-areatype="toolbar"]:not([overflowedItem=true]) > #zoom-reset-button > .toolbarbutton-text {
   display: -moz-box;
 }
 
 #urlbar-reload-button:not([displaystop]) + #urlbar-stop-button,
 #urlbar-reload-button[displaystop] {
   visibility: collapse;
 }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -939,17 +939,16 @@
              mode="icons" iconsize="small"
              class="chromeclass-directories"
              context="toolbar-context-menu"
              defaultset="personal-bookmarks"
              toolbarname="&personalbarCmd.label;" accesskey="&personalbarCmd.accesskey;"
              collapsed="true"
              customizable="true">
       <toolbaritem id="personal-bookmarks"
-                   flex="1"
                    title="&bookmarksToolbarItem.label;"
                    cui-areatype="toolbar"
                    removable="true">
         <toolbarbutton id="bookmarks-toolbar-placeholder"
                        class="toolbarbutton-1"
                        mousethrough="never"
                        label="&bookmarksToolbarItem.label;"
                        oncommand="PlacesToolbarHelper.onPlaceholderCommand();"/>
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -193,17 +193,17 @@ let gGrid = {
       let refCell = document.querySelector(".newtab-cell");
       this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
       this._cellHeight = refCell.offsetHeight + this._cellMargin;
       this._cellWidth = refCell.offsetWidth + this._cellMargin;
     }
 
     let availSpace = document.documentElement.clientHeight - this._cellMargin -
                      document.querySelector("#newtab-margin-undo-container").offsetHeight -
-                     document.querySelector("#newtab-search-form").offsetHeight;
+                     document.querySelector("#newtab-search-container").offsetHeight;
     let visibleRows = Math.floor(availSpace / this._cellHeight);
     this._node.style.height = this._computeHeight() + "px";
     this._node.style.maxHeight = this._computeHeight(visibleRows) + "px";
     this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth +
                                 GRID_WIDTH_EXTRA + "px";
 
     // Resize the search bar.
     let width = parseFloat(window.getComputedStyle(this._node).width);
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -23,16 +23,17 @@ input[type=button] {
 #newtab-scrollbox:not([page-disabled]) {
   overflow: auto;
 }
 
 /* UNDO */
 #newtab-undo-container {
   transition: opacity 100ms ease-out;
   display: -moz-box;
+  margin-bottom: 26px; /* 32 - 6 search form top "padding" */
   -moz-box-align: center;
   -moz-box-pack: center;
 }
 
 #newtab-undo-container[undo-disabled] {
   opacity: 0;
   pointer-events: none;
 }
@@ -55,17 +56,16 @@ input[type=button] {
   position: relative;
   -moz-box-flex: 1;
   -moz-box-orient: vertical;
 }
 
 #newtab-margin-undo-container {
   display: -moz-box;
   -moz-box-pack: center;
-  margin-bottom: 26px; /* 32 - 6 search form top "padding" */
 }
 
 #newtab-horizontal-margin {
   display: -moz-box;
   -moz-box-flex: 1;
 }
 
 #newtab-margin-top,
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -14,16 +14,18 @@ Cu.import("resource://gre/modules/Backgr
 Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm");
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
+  "resource://gre/modules/UpdateChannel.jsm");
 
 let {
   links: gLinks,
   allPages: gAllPages,
   linkChecker: gLinkChecker,
   pinnedLinks: gPinnedLinks,
   blockedLinks: gBlockedLinks,
   gridPrefs: gGridPrefs
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -15,17 +15,18 @@
   %searchBarDTD;
 ]>
 
 <xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml"
             xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             title="&newtab.pageTitle;">
 
   <xul:panel id="sponsored-panel" orient="vertical" type="arrow">
-    <xul:description>&newtab.panel.message;</xul:description>
+    <xul:description id="sponsored-panel-release-descr">&newtab.sponsored.release.message;</xul:description>
+    <xul:description id="sponsored-panel-trial-descr">&newtab.sponsored.trial.message;</xul:description>
     <xul:label class="text-link"
                href="https://support.mozilla.org/kb/how-do-sponsored-tiles-work"
                value="&newtab.panel.link.text;" />
   </xul:panel>
 
   <xul:panel id="newtab-search-panel" orient="vertical" type="arrow"
              noautohide="true">
     <xul:hbox id="newtab-search-manage" class="newtab-search-panel-engine">
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -23,16 +23,22 @@ let gPage = {
     // listening from xhtml nodes -- in particular middle clicks on sites, so
     // listen from the xul window and filter then delegate
     addEventListener("click", this, false);
 
     // Initialize sponsored panel
     this._sponsoredPanel = document.getElementById("sponsored-panel");
     let link = this._sponsoredPanel.querySelector(".text-link");
     link.addEventListener("click", () => this._sponsoredPanel.hidePopup());
+    if (UpdateChannel.get().startsWith("release")) {
+      document.getElementById("sponsored-panel-trial-descr").style.display = "none";
+    }
+    else {
+      document.getElementById("sponsored-panel-release-descr").style.display = "none";
+    }
 
     // Check if the new tab feature is enabled.
     let enabled = gAllPages.enabled;
     if (enabled)
       this._init();
 
     this._updateAttributes(enabled);
   },
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -160,16 +160,25 @@ let CustomizableUIInternal = {
       "add-ons-button",
       "developer-button",
     ];
 
     if (gPalette.has("switch-to-metro-button")) {
       panelPlacements.push("switch-to-metro-button");
     }
 
+#ifdef NIGHTLY_BUILD
+    if (gPalette.has("e10s-button")) {
+      let newWindowIndex = panelPlacements.indexOf("new-window-button");
+      if (newWindowIndex > -1) {
+        panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button");
+      }
+    }
+#endif
+
     let showCharacterEncoding = Services.prefs.getComplexValue(
       "browser.menu.showCharacterEncoding",
       Ci.nsIPrefLocalizedString
     ).data;
     if (showCharacterEncoding == "true") {
       panelPlacements.push("characterencoding-button");
     }
 
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -907,8 +907,41 @@ if (Services.metro && Services.metro.sup
       if (win && typeof win.SwitchToMetro == "function") {
         win.SwitchToMetro();
       }
     }
   });
 }
 #endif
 #endif
+
+#ifdef NIGHTLY_BUILD
+/**
+ * The e10s button's purpose is to lower the barrier of entry
+ * for our Nightly testers to use e10s windows. We'll be removing it
+ * once remote tabs are enabled. This button should never ever make it
+ * to production. If it does, that'd be bad, and we should all feel bad.
+ */
+if (Services.prefs.getBoolPref("browser.tabs.remote")) {
+  let getCommandFunction = function(aOpenRemote) {
+    return function(aEvent) {
+      let win = aEvent.view;
+      if (win && typeof win.OpenBrowserWindow == "function") {
+        win.OpenBrowserWindow({remote: aOpenRemote});
+      }
+    };
+  }
+
+  let openRemote = !Services.prefs.getBoolPref("browser.tabs.remote.autostart");
+  // Like the XUL menuitem counterparts, we hard-code these strings in because
+  // this button should never roll into production.
+  let buttonLabel = openRemote ? "New e10s Window"
+                               : "New Non-e10s Window";
+
+  CustomizableWidgets.push({
+    id: "e10s-button",
+    label: buttonLabel,
+    tooltiptext: buttonLabel,
+    defaultArea: CustomizableUI.AREA_PANEL,
+    onCommand: getCommandFunction(openRemote),
+  });
+}
+#endif
\ No newline at end of file
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -60,29 +60,16 @@ skip-if = e10s # Bug ?????? - test uses 
 [browser_940107_home_button_in_bookmarks_toolbar.js]
 [browser_940307_panel_click_closure_handling.js]
 [browser_940946_removable_from_navbar_customizemode.js]
 [browser_941083_invalidate_wrapper_cache_createWidget.js]
 [browser_942581_unregisterArea_keeps_placements.js]
 [browser_943683_migration_test.js]
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
 [browser_945739_showInPrivateBrowsing_customize_mode.js]
-[browser_947914_button_addons.js]
-[browser_947914_button_copy.js]
-[browser_947914_button_cut.js]
-[browser_947914_button_find.js]
-[browser_947914_button_history.js]
-[browser_947914_button_newPrivateWindow.js]
-[browser_947914_button_newWindow.js]
-[browser_947914_button_paste.js]
-[browser_947914_button_print.js]
-[browser_947914_button_savePage.js]
-[browser_947914_button_zoomIn.js]
-[browser_947914_button_zoomOut.js]
-[browser_947914_button_zoomReset.js]
 [browser_947987_removable_default.js]
 [browser_948985_non_removable_defaultArea.js]
 [browser_952963_areaType_getter_no_area.js]
 [browser_956602_remove_special_widget.js]
 [browser_962884_opt_in_disable_hyphens.js]
 [browser_963639_customizing_attribute_non_customizable_toolbar.js]
 [browser_967000_button_charEncoding.js]
 skip-if = e10s # Bug ?????? - test uses promiseTabLoadEvent() which isn't e10s friendly.
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_addons.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check addons button existence and functionality");
-
-  let initialLocation = gBrowser.currentURI.spec;
-
-  yield PanelUI.show();
-
-  let addonsButton = document.getElementById("add-ons-button");
-  ok(addonsButton, "Add-ons button exists in Panel Menu");
-  addonsButton.click();
-
-  yield waitForCondition(function() gBrowser.currentURI &&
-                                    gBrowser.currentURI.spec == "about:addons");
-
-  let addonsPage = gBrowser.selectedBrowser.contentWindow.document.
-                            getElementById("addons-page");
-  ok(addonsPage, "Add-ons page was opened");
-
-  // close the add-ons tab
-  if(gBrowser.tabs.length > 1) {
-    gBrowser.removeTab(gBrowser.selectedTab);
-  }
-  else {
-    var tabToRemove = gBrowser.selectedTab;
-    gBrowser.addTab(initialLocation);
-    gBrowser.removeTab(tabToRemove);
-  }
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_copy.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check copy button existence and functionality");
-
-  var testText = "copy text test";
-  let initialLocation = gBrowser.currentURI.spec;
-
-  yield PanelUI.show();
-
-  let copyButton = document.getElementById("copy-button");
-  ok(copyButton, "Copy button exists in Panel Menu");
-  is(copyButton.getAttribute("disabled"), "true", "Copy button is initially disabled");
-
-  // copy text from URL bar
-  gURLBar.value = testText;
-  gURLBar.focus();
-  gURLBar.select();
-  yield PanelUI.show();
-
-  ok(!copyButton.hasAttribute("disabled"), "Copy button gets enabled");
-
-  copyButton.click();
-  is(gURLBar.value, testText, "Selected text is unaltered when clicking copy");
-
-  // check that the text was added to the clipboard
-  let clipboard = Services.clipboard;
-  let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
-  const globalClipboard = clipboard.kGlobalClipboard;
-
-  transferable.init(null);
-  transferable.addDataFlavor("text/unicode");
-  clipboard.getData(transferable, globalClipboard);
-  let str = {}, strLength = {};
-  transferable.getTransferData("text/unicode", str, strLength);
-  let clipboardValue = "";
-
-  if (str.value) {
-    str.value.QueryInterface(Ci.nsISupportsString);
-    clipboardValue = str.value.data;
-  }
-  is(clipboardValue, testText, "Data was copied to the clipboard.");
-
-  // restore the tab location and clear the clipboard
-  Services.clipboard.emptyClipboard(globalClipboard);
-  gURLBar.value = initialLocation;
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_cut.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check cut button existence and functionality");
-
-  var testText = "cut text test";
-  let initialLocation = gBrowser.currentURI.spec;
-
-  yield PanelUI.show();
-
-  let cutButton = document.getElementById("cut-button");
-  ok(cutButton, "Cut button exists in Panel Menu");
-  ok(cutButton.getAttribute("disabled"), "Cut button is disabled");
-
-  // cut text from URL bar
-  gURLBar.value = testText;
-  gURLBar.focus();
-  gURLBar.select();
-  yield PanelUI.show();
-
-  ok(!cutButton.hasAttribute("disabled"), "Cut button gets enabled");
-  cutButton.click();
-  is(gURLBar.value, "", "Selected text is removed from source when clicking on cut");
-
-  // check that the text was added to the clipboard
-  let clipboard = Services.clipboard;
-  let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
-  const globalClipboard = clipboard.kGlobalClipboard;
-
-  transferable.init(null);
-  transferable.addDataFlavor("text/unicode");
-  clipboard.getData(transferable, globalClipboard);
-  let str = {}, strLength = {};
-  transferable.getTransferData("text/unicode", str, strLength);
-  let clipboardValue = "";
-
-  if (str.value) {
-    str.value.QueryInterface(Ci.nsISupportsString);
-    clipboardValue = str.value.data;
-  }
-  is(clipboardValue, testText, "Data was copied to the clipboard.");
-
-  // restore the tab location and clear the clipboard
-  gBrowser.value = initialLocation;
-  Services.clipboard.emptyClipboard(globalClipboard);
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_find.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check find button existence and functionality");
-
-  yield PanelUI.show();
-
-  let findButton = document.getElementById("find-button");
-  ok(findButton, "Find button exists in Panel Menu");
-
-  findButton.click();
-  ok(!gFindBar.hasAttribute("hidden"), "Findbar opened successfully");
-
-  // close find bar
-  gFindBar.close();
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_history.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check history button existence and functionality");
-
-  yield PanelUI.show();
-
-  let historyButton = document.getElementById("history-panelmenu");
-  ok(historyButton, "History button appears in Panel Menu");
-
-  historyButton.click();
-  let historyPanel = document.getElementById("PanelUI-history");
-  ok(historyPanel.getAttribute("current"), "History Panel is in view");
-
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check private browsing button existence and functionality");
-  yield PanelUI.show();
-
-  var windowWasHandled = false;
-  let privateWindow = null;
-
-  let observerWindowOpened = {
-    observe: function(aSubject, aTopic, aData) {
-      if (aTopic == "domwindowopened") {
-        privateWindow = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
-        privateWindow.addEventListener("load", function newWindowHandler() {
-          privateWindow.removeEventListener("load", newWindowHandler, false);
-          is(privateWindow.location.href, "chrome://browser/content/browser.xul",
-             "A new browser window was opened");
-          ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "Window is private");
-          windowWasHandled = true;
-        }, false);
-      }
-    }
-  }
-
-  Services.ww.registerNotification(observerWindowOpened);
-
-  let privateBrowsingButton = document.getElementById("privatebrowsing-button");
-  ok(privateBrowsingButton, "Private browsing button exists in Panel Menu");
-  privateBrowsingButton.click();
-
-  try{
-    yield waitForCondition(() => windowWasHandled);
-    yield promiseWindowClosed(privateWindow);
-  }
-  catch(e) {
-    ok(false, "The new private browser window was not properly handled");
-  }
-  finally {
-    Services.ww.unregisterNotification(observerWindowOpened);
-  }
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_newWindow.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check new window button existence and functionality");
-  yield PanelUI.show();
-
-  var windowWasHandled = false;
-  var newWindow = null;
-
-  let observerWindowOpened = {
-    observe: function(aSubject, aTopic, aData) {
-      if (aTopic == "domwindowopened") {
-        newWindow = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
-        newWindow.addEventListener("load", function newWindowHandler() {
-          newWindow.removeEventListener("load", newWindowHandler, false);
-          is(newWindow.location.href, "chrome://browser/content/browser.xul",
-             "A new browser window was opened");
-          ok(!PrivateBrowsingUtils.isWindowPrivate(newWindow), "Window is not private");
-          windowWasHandled = true;
-        }, false);
-      }
-    }
-  }
-
-  Services.ww.registerNotification(observerWindowOpened);
-
-  let newWindowButton = document.getElementById("new-window-button");
-  ok(newWindowButton, "New Window button exists in Panel Menu");
-  newWindowButton.click();
-
-  try{
-    yield waitForCondition(() => windowWasHandled);
-    yield promiseWindowClosed(newWindow);
-  }
-  catch(e) {
-    ok(false, "The new browser window was not properly handled");
-  }
-  finally {
-    Services.ww.unregisterNotification(observerWindowOpened);
-  }
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_paste.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check paste button existence and functionality");
-
-  let initialLocation = gBrowser.currentURI.spec;
-  let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
-  const globalClipboard = Services.clipboard.kGlobalClipboard;
-
-  yield PanelUI.show();
-
-  let pasteButton = document.getElementById("paste-button");
-  ok(pasteButton, "Paste button exists in Panel Menu");
-
-  // add text to clipboard
-  var text = "Sample text for testing";
-  clipboard.copyString(text);
-
-  // test paste button by pasting text to URL bar
-  gURLBar.focus();
-  yield PanelUI.show();
-
-  ok(!pasteButton.hasAttribute("disabled"), "Paste button is enabled");
-  pasteButton.click();
-
-  is(gURLBar.value, text, "Text pasted successfully");
-
-  // clear the clipboard and restore the tab location as it was at the begining of the test
-  Services.clipboard.emptyClipboard(globalClipboard);
-  gURLBar.value = initialLocation;
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_print.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* 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";
-
-const isOSX = (Services.appinfo.OS === "Darwin");
-
-add_task(function() {
-  info("Check print button existence and functionality");
-
-  yield PanelUI.show();
-
-  let printButton = document.getElementById("print-button");
-  ok(printButton, "Print button exists in Panel Menu");
-
-  if(isOSX) {
-    let panelHiddenPromise = promisePanelHidden(window);
-    PanelUI.hide();
-    yield panelHiddenPromise;
-  }
-  else {
-    printButton.click();
-    yield waitForCondition(() => window.gInPrintPreviewMode);
-
-    ok(window.gInPrintPreviewMode, "Entered print preview mode");
-
-    // close print preview
-    PrintUtils.exitPrintPreview();
-    yield waitForCondition(() => !window.gInPrintPreviewMode);
-  }
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_savePage.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check save page button existence");
-
-  yield PanelUI.show();
-
-  let savePageButton = document.getElementById("save-page-button");
-  ok(savePageButton, "Save Page button exists in Panel Menu");
-
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise;
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_zoomIn.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check zoom in button existence and functionality");
-
-  let initialPageZoom = ZoomManager.zoom;
-  is(initialPageZoom, 1, "Initial zoom factor should be 1");
-
-  yield PanelUI.show();
-
-  let zoomInButton = document.getElementById("zoom-in-button");
-  ok(zoomInButton, "Zoom in button exists in Panel Menu");
-
-  zoomInButton.click();
-  let pageZoomLevel = parseInt(ZoomManager.zoom * 100);
-  let zoomResetButton = document.getElementById("zoom-reset-button");
-  let expectedZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10);
-  ok(pageZoomLevel > 100 && pageZoomLevel == expectedZoomLevel, "Page zoomed in correctly");
-
-  // close the Panel
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise;
-
-  // reset zoom level
-  ZoomManager.zoom = initialPageZoom;
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_zoomOut.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check zoom out button existence and functionality");
-
-  let initialPageZoom = ZoomManager.zoom;
-  is(initialPageZoom, 1, "Initial zoom factor should be 1");
-
-  yield PanelUI.show();
-
-  let zoomOutButton = document.getElementById("zoom-out-button");
-  ok(zoomOutButton, "Zoom out button exists in Panel Menu");
-
-  zoomOutButton.click();
-  let pageZoomLevel = Math.round(ZoomManager.zoom*100);
-
-  let zoomResetButton = document.getElementById("zoom-reset-button");
-  let expectedZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10);
-  ok(pageZoomLevel < 100 && pageZoomLevel == expectedZoomLevel, "Page zoomed out correctly");
-
-  // close the panel
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise;
-
-  // reset zoom level
-  ZoomManager.zoom = initialPageZoom;
-});
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_947914_button_zoomReset.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/* 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";
-
-add_task(function() {
-  info("Check zoom reset button existence and functionality");
-
-  let initialPageZoom = ZoomManager.zoom;
-  is(initialPageZoom, 1, "Initial zoom factor should be 1");
-  ZoomManager.zoom = 0.5;
-  yield PanelUI.show();
-
-  let zoomResetButton = document.getElementById("zoom-reset-button");
-  ok(zoomResetButton, "Zoom reset button exists in Panel Menu");
-
-  zoomResetButton.click();
-  let pageZoomLevel = parseInt(ZoomManager.zoom * 100);
-  let expectedZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10);
-  ok(pageZoomLevel == expectedZoomLevel && pageZoomLevel == 100, "Page zoom reset correctly");
-
-  // close the panel
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise;
-
-  //reset the zoom level
-  ZoomManager.zoom = initialPageZoom;
-});
--- a/browser/components/preferences/in-content/applications.js
+++ b/browser/components/preferences/in-content/applications.js
@@ -1682,18 +1682,20 @@ var gApplicationsPane = {
   manageApp: function(aEvent) {
     // Don't let the normal "on select action" handler get this event,
     // as we handle it specially ourselves.
     aEvent.stopPropagation();
 
     var typeItem = this._list.selectedItem;
     var handlerInfo = this._handledTypes[typeItem.type];
 
-    document.documentElement.openSubDialog("chrome://browser/content/preferences/applicationManager.xul",
-                                           "", handlerInfo);
+    openDialog("chrome://browser/content/preferences/applicationManager.xul",
+               "",
+               "modal,centerscreen,resizable=no",
+               handlerInfo);
 
     // Rebuild the actions menu so that we revert to the previous selection,
     // or "Always ask" if the previous default application has been removed
     this.rebuildActionsMenu();
 
     // update the richlistitem too. Will be visible when selecting another row
     typeItem.setAttribute("actionDescription",
                           this._describePreferredAction(handlerInfo));
--- a/browser/devtools/eyedropper/eyedropper.js
+++ b/browser/devtools/eyedropper/eyedropper.js
@@ -110,16 +110,18 @@ function Eyedropper(chromeWindow, opts =
   this._onFrameLoaded = this._onFrameLoaded.bind(this);
 
   this._chromeWindow = chromeWindow;
   this._chromeDocument = chromeWindow.document;
 
   this._dragging = true;
   this.loaded = false;
 
+  this._mouseMoveCounter = 0;
+
   this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format
   this.zoom = Services.prefs.getIntPref(ZOOM_PREF);      // zoom level - integer
 
   this._zoomArea = {
     x: 0,          // the left coordinate of the center of the inspected region
     y: 0,          // the top coordinate of the center of the inspected region
     width: CANVAS_WIDTH,      // width of canvas to draw zoomed area onto
     height: CANVAS_WIDTH      // height of canvas
@@ -373,16 +375,21 @@ Eyedropper.prototype = {
    * @param  {DOMEvent} event
    *         MouseEvent for the mouse moving
    */
   _onMouseMove: function(event) {
     if (!this._dragging || !this._panel || !this._canvas) {
       return;
     }
 
+    if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) {
+      // skip every other mousemove to preserve performance.
+      return;
+    }
+
     this._setCoordinates(event);
     this._drawWindow();
 
     let { panelX, panelY } = this._getPanelCoordinates(event);
     this._movePanel(panelX, panelY);
   },
 
   /**
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -1031,39 +1031,42 @@ function detectIndentation(ed) {
       return;
     }
     if (width > 1) {
       total++;
     }
 
     // see how much this line is offset from the line above it
     let indent = Math.abs(width - last);
-    if (indent > 1) {
+    if (indent > 1 && indent <= 8) {
       spaces[indent] = (spaces[indent] || 0) + 1;
     }
     last = width;
   });
 
   // this file is not indented at all
   if (total == 0) {
     return null;
   }
 
   // mark as tabs if they start more than half the lines
   if (tabs >= total / 2) {
     return { tabs: true };
   }
 
   // find most frequent non-zero width difference between adjacent lines
-  let freqIndent = null, max = 0;
+  let freqIndent = null, max = 1;
   for (let width in spaces) {
     width = parseInt(width, 10);
     let tally = spaces[width];
     if (tally > max) {
       max = tally;
       freqIndent = width;
     }
   }
+  if (!freqIndent) {
+    return null;
+  }
 
   return { tabs: false, spaces: freqIndent };
 }
 
 module.exports = Editor;
--- a/browser/devtools/sourceeditor/test/browser_detectindent.js
+++ b/browser/devtools/sourceeditor/test/browser_detectindent.js
@@ -52,26 +52,39 @@ const TABS_CODE = [
 "\tbackground: blue;",
 "}",
 "",
 "span {",
 "\tpadding-left: 10px;",
 "}"
 ].join("\n");
 
+const NONE_CODE = [
+"var x = 0;",
+"           // stray thing",
+"var y = 9;",
+"    ",
+""
+].join("\n");
 
 function test() {
   waitForExplicitFinish();
 
   setup((ed, win) => {
     is(ed.getOption("indentUnit"), 2,
        "2 spaces before code added");
     is(ed.getOption("indentWithTabs"), false,
        "spaces is default");
 
+    ed.setText(NONE_CODE);
+    is(ed.getOption("indentUnit"), 2,
+       "2 spaces after un-detectable code");
+    is(ed.getOption("indentWithTabs"), false,
+       "spaces still set after un-detectable code");
+
     ed.setText(FOUR_SPACES_CODE);
     is(ed.getOption("indentUnit"), 4,
        "4 spaces detected in 4 space code");
     is(ed.getOption("indentWithTabs"), false,
        "spaces detected in 4 space code");
 
     ed.setText(TWO_SPACES_CODE);
     is(ed.getOption("indentUnit"), 2,
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -63,19 +63,16 @@ const COMPAT = {
   CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
                               "input", "output", "security" ],
 
   // The fragment of a CSS class name that identifies each severity.
   SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
 
   // The indent of a console group in pixels.
   GROUP_INDENT: 12,
-
-  // The default indent in pixels, applied even without any groups.
-  GROUP_INDENT_DEFAULT: 6,
 };
 
 // A map from the console API call levels to the Web Console severities.
 const CONSOLE_API_LEVELS_TO_SEVERITIES = {
   error: "error",
   exception: "error",
   assert: "error",
   warn: "warning",
@@ -812,32 +809,34 @@ Messages.Simple.prototype = Heritage.ext
 
     let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
 
     let icon = this.document.createElementNS(XHTML_NS, "span");
     icon.className = "icon";
 
     // Apply the current group by indenting appropriately.
     // TODO: remove this once bug 778766 is fixed.
-    let iconMarginLeft = this._groupDepthCompat * COMPAT.GROUP_INDENT +
-                         COMPAT.GROUP_INDENT_DEFAULT;
-    icon.style.marginLeft = iconMarginLeft + "px";
+    let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
+    let indentNode = this.document.createElementNS(XHTML_NS, "span");
+    indentNode.className = "indent";
+    indentNode.style.width = indent + "px";
 
     let body = this._renderBody();
     this._repeatID.textContent += "|" + body.textContent;
 
     let repeatNode = this._renderRepeatNode();
     let location = this._renderLocation();
 
     Messages.BaseMessage.prototype.render.call(this);
     if (this._className) {
       this.element.className += " " + this._className;
     }
 
     this.element.appendChild(timestamp.element);
+    this.element.appendChild(indentNode);
     this.element.appendChild(icon);
     this.element.appendChild(body);
     if (repeatNode) {
       this.element.appendChild(repeatNode);
     }
     if (location) {
       this.element.appendChild(location);
     }
@@ -1105,24 +1104,24 @@ Messages.Extended.prototype = Heritage.e
    *        Value grip from the server.
    * @return string
    *         The class name for the grip.
    */
   getClassNameForValueGrip: function(grip)
   {
     let map = {
       "number": "cm-number",
-      "longstring": "cm-string",
-      "string": "cm-string",
+      "longstring": "console-string",
+      "string": "console-string",
       "regexp": "cm-string-2",
       "boolean": "cm-atom",
       "-infinity": "cm-atom",
       "infinity": "cm-atom",
       "null": "cm-atom",
-      "undefined": "cm-atom",
+      "undefined": "cm-comment",
     };
 
     let className = map[typeof grip];
     if (!className && grip && grip.type) {
       className = map[grip.type.toLowerCase()];
     }
     if (!className && grip && grip.class) {
       className = map[grip.class.toLowerCase()];
@@ -2112,18 +2111,18 @@ Widgets.ObjectRenderers.add({
       }
 
       let keyElem = this.message._renderValueGrip(key, {
         concise: true,
         noStringQuotes: true,
       });
 
       // Strings are property names.
-      if (keyElem.classList && keyElem.classList.contains("cm-string")) {
-        keyElem.classList.remove("cm-string");
+      if (keyElem.classList && keyElem.classList.contains("console-string")) {
+        keyElem.classList.remove("console-string");
         keyElem.classList.add("cm-property");
       }
 
       container.appendChild(keyElem);
 
       this._text(": ");
 
       let valueElem = this.message._renderValueGrip(value, { concise: true });
@@ -2187,17 +2186,17 @@ Widgets.ObjectRenderers.add({
   {
     let {preview} = this.objectActor;
     this.element = this.el("span.kind-" + preview.kind);
 
     this._anchor(this.objectActor.class, { className: "cm-variable" });
 
     if (!this.options.concise) {
       this._text(" ");
-      this.element.appendChild(this.el("span.cm-string",
+      this.element.appendChild(this.el("span.console-string",
                                        VariablesView.getString(preview.text)));
     }
   },
 });
 
 /**
  * The widget used for displaying DOM event previews.
  */
@@ -2331,31 +2330,32 @@ Widgets.ObjectRenderers.add({
     let fragment = this.document.createDocumentFragment();
     if (addLink) {
       this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
     } else {
       fragment.appendChild(this.el("span.cm-attribute", nodeName));
     }
 
     this._text("=", fragment);
-    fragment.appendChild(this.el("span.cm-string", '"' + escapeHTML(value) + '"'));
+    fragment.appendChild(this.el("span.console-string",
+                                 '"' + escapeHTML(value) + '"'));
 
     return fragment;
   },
 
   _renderTextNode: function()
   {
     let {preview} = this.objectActor;
     this.element = this.el("span.textNode.kind-" + preview.kind);
 
     this._anchor(preview.nodeName, { className: "cm-variable" });
     this._text(" ");
 
     let text = VariablesView.getString(preview.textContent);
-    this.element.appendChild(this.el("span.cm-string", text));
+    this.element.appendChild(this.el("span.console-string", text));
   },
 
   _renderCommentNode: function()
   {
     let {preview} = this.objectActor;
     let comment = "<!-- " + VariablesView.getString(preview.textContent, {
       noStringQuotes: true,
     }) + " -->";
@@ -2673,17 +2673,17 @@ Widgets.LongString.prototype = Heritage.
 
   render: function()
   {
     if (this.element) {
       return this;
     }
 
     let result = this.element = this.document.createElementNS(XHTML_NS, "span");
-    result.className = "longString cm-string";
+    result.className = "longString console-string";
     this._renderString(this.longStringActor.initial);
     result.appendChild(this._renderEllipsis());
 
     return this;
   },
 
   /**
    * Render the long string in the widget element.
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -31,19 +31,16 @@ const CATEGORY_SECURITY = 6;
 const SEVERITY_ERROR = 0;
 const SEVERITY_WARNING = 1;
 const SEVERITY_INFO = 2;
 const SEVERITY_LOG = 3;
 
 // The indent of a console group in pixels.
 const GROUP_INDENT = 12;
 
-// The default indent in pixels, applied even without any groups.
-const GROUP_INDENT_DEFAULT = 6;
-
 const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI);
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
@@ -1199,20 +1196,20 @@ function waitForMessages(aOptions)
     if ("repeats" in aRule) {
       let repeats = aElement.querySelector(".message-repeats");
       if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
         return false;
       }
     }
 
     if ("groupDepth" in aRule) {
-      let icon = aElement.querySelector(".icon");
-      let indent = (GROUP_INDENT * aRule.groupDepth + GROUP_INDENT_DEFAULT) + "px";
-      if (!icon || icon.style.marginLeft != indent) {
-        is(icon.style.marginLeft, indent,
+      let indentNode = aElement.querySelector(".indent");
+      let indent = (GROUP_INDENT * aRule.groupDepth)  + "px";
+      if (!indentNode || indentNode.style.width != indent) {
+        is(indentNode.style.width, indent,
            "group depth check failed for message rule: " + displayRule(aRule));
         return false;
       }
     }
 
     if ("longString" in aRule) {
       let longStrings = aElement.querySelectorAll(".longStringEllipsis");
       if (aRule.longString != !!longStrings[0]) {
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -140,19 +140,16 @@ const MAX_HTTP_ERROR_CODE = 599;
 
 // Constants used for defining the direction of JSTerm input history navigation.
 const HISTORY_BACK = -1;
 const HISTORY_FORWARD = 1;
 
 // The indent of a console group in pixels.
 const GROUP_INDENT = 12;
 
-// The default indent in pixels, applied even without any groups.
-const GROUP_INDENT_DEFAULT = 6;
-
 // The number of messages to display in a single display update. If we display
 // too many messages at once we slow the Firefox UI too much.
 const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
 
 // The delay between display updates - tells how often we should *try* to push
 // new messages to screen. This value is optimistic, updates won't always
 // happen. Keep this low so the Web Console output feels live.
 const OUTPUT_INTERVAL = 50; // milliseconds
@@ -2444,26 +2441,29 @@ WebConsoleFrame.prototype = {
   createMessageNode:
   function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
                                  aSourceLine, aClipboardText, aLevel, aTimeStamp)
   {
     if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
       aClipboardText = aBody.innerText;
     }
 
+    let indentNode = this.document.createElementNS(XHTML_NS, "span");
+    indentNode.className = "indent";
+
+    // Apply the current group by indenting appropriately.
+    let indent = this.groupDepth * GROUP_INDENT;
+    indentNode.style.width = indent + "px";
+
     // Make the icon container, which is a vertical box. Its purpose is to
     // ensure that the icon stays anchored at the top of the message even for
     // long multi-line messages.
     let iconContainer = this.document.createElementNS(XHTML_NS, "span");
     iconContainer.className = "icon";
 
-    // Apply the current group by indenting appropriately.
-    let iconMarginLeft = this.groupDepth * GROUP_INDENT + GROUP_INDENT_DEFAULT;
-    iconContainer.style.marginLeft = iconMarginLeft + "px";
-
     // Create the message body, which contains the actual text of the message.
     let bodyNode = this.document.createElementNS(XHTML_NS, "span");
     bodyNode.className = "message-body-wrapper message-body devtools-monospace";
 
     // Store the body text, since it is needed later for the variables view.
     let body = aBody;
     // If a string was supplied for the body, turn it into a DOM node and an
     // associated clipboard string now.
@@ -2523,16 +2523,17 @@ WebConsoleFrame.prototype = {
     // Create the source location (e.g. www.example.com:6) that sits on the
     // right side of the message, if applicable.
     let locationNode;
     if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
       locationNode = this.createLocationNode(aSourceURL, aSourceLine);
     }
 
     node.appendChild(timestampNode);
+    node.appendChild(indentNode);
     node.appendChild(iconContainer);
 
     // Display the variables view after the message node.
     if (aLevel == "dir") {
       bodyNode.style.height = (this.window.innerHeight *
                                CONSOLE_DIR_VIEW_HEIGHT) + "px";
 
       let options = {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -254,17 +254,17 @@ These should match what Safari and other
   -  application menu item that opens the browser toolbox UI in the Tools menu. -->
 <!ENTITY browserToolboxMenu.label     "Browser Toolbox">
 <!ENTITY browserToolboxMenu.accesskey "e">
 
 <!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
 <!ENTITY devToolbarMenu.label              "Developer Toolbar">
 <!ENTITY devToolbarMenu.accesskey          "v">
 <!ENTITY devAppMgrMenu.label               "App Manager">
-<!ENTITY devAppMgrMenu.accesskey           "a">
+<!ENTITY devAppMgrMenu.accesskey           "A">
 <!ENTITY devToolbar.keycode                "VK_F2">
 <!ENTITY devToolbar.keytext                "F2">
 <!ENTITY devToolboxMenuItem.label          "Toggle Tools">
 <!ENTITY devToolboxMenuItem.accesskey      "T">
 <!ENTITY devToolboxMenuItem.keytext        "I">
 
 <!ENTITY devToolbarToolsButton.tooltip     "Toggle developer tools">
 <!ENTITY devToolbarOtherToolsButton.label  "More Tools">
--- a/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties
@@ -53,17 +53,17 @@ saveStyleSheet.title=Save style sheet
 saveStyleSheet.filter=CSS files
 
 # LOCALIZATION NOTE  (open.commandkey): This the key to use in
 # conjunction with shift to open the style editor
 open.commandkey=VK_F7
 
 # LOCALIZATION NOTE (open.accesskey): The access key used to open the style
 # editor.
-open.accesskey=y
+open.accesskey=l
 
 # LOCALIZATION NOTE  (saveStyleSheet.commandkey): This the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to Save
 saveStyleSheet.commandkey=S
 
 # LOCALIZATION NOTE  (showOriginalSources.label): This is the label on the context
 # menu item to toggle showing original sources in the editor.
 showOriginalSources.label=Show original sources
--- a/browser/locales/en-US/chrome/browser/newTab.dtd
+++ b/browser/locales/en-US/chrome/browser/newTab.dtd
@@ -3,10 +3,11 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!-- These strings are used in the about:newtab page -->
 <!ENTITY newtab.pageTitle "New Tab">
 <!ENTITY newtab.undo.removedLabel "Thumbnail removed.">
 <!ENTITY newtab.undo.undoButton "Undo.">
 <!ENTITY newtab.undo.restoreButton "Restore All.">
 <!ENTITY newtab.undo.closeTooltip "Hide">
-<!ENTITY newtab.panel.message "This site is being suggested because it has sponsored Mozilla, helping us promote openness, innovation and opportunity on the Web.">
+<!ENTITY newtab.sponsored.release.message "This Sponsor site was suggested because we hoped you’d find it interesting and because it supports Mozilla’s mission.">
+<!ENTITY newtab.sponsored.trial.message "This Trial Sponsor site was suggested because we hoped you’d find it interesting and because it supports Mozilla’s mission.">
 <!ENTITY newtab.panel.link.text "Learn more…">
--- a/browser/themes/linux/preferences/in-content/preferences.css
+++ b/browser/themes/linux/preferences/in-content/preferences.css
@@ -21,37 +21,33 @@ menulist:not([editable="true"]) > .menul
 
 checkbox {
   -moz-binding: url("chrome://global/content/bindings/checkbox.xml#checkbox");
   -moz-box-align: center;
 }
 
 checkbox:hover::before,
 checkbox[checked]::before {
-  margin-top: 7px;
-  margin-bottom: 6px;
   -moz-margin-end: -19px;
   -moz-margin-start: 4px;
   background-repeat: no-repeat;
 }
 
 radio {
   -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
   -moz-box-align: center;
-  margin: 7px 0;
+  -moz-margin-start: 0;
 }
 
 .radio-label-box {
   -moz-appearance: none;
 }
 
 radio:hover::before,
 radio[selected]::before {
-  margin-top: 6px;
-  margin-bottom: 6px;
   -moz-margin-end: -17px;
   -moz-margin-start: 6px;
 }
 
 .numberbox-input-box {
   -moz-appearance: none;
   border-width: 0;
 }
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -696,16 +696,20 @@ toolbar .toolbarbutton-1 > .toolbarbutto
   #characterencoding-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 324px, 36px, 306px);
   }
 
   #new-window-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 342px, 36px, 324px);
   }
 
+  #e10s-button@toolbarButtonPressed@ {
+    -moz-image-region: rect(18px, 342px, 36px, 324px);
+  }
+
   #new-tab-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 360px, 36px, 342px);
   }
 
   #privatebrowsing-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 378px, 36px, 360px);
   }
 
@@ -937,16 +941,28 @@ toolbar .toolbarbutton-1 > .toolbarbutto
   #new-window-button[cui-areatype="toolbar"] {
     -moz-image-region: rect(0, 684px, 36px, 648px);
   }
 
   #new-window-button[cui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 684px, 72px, 648px);
   }
 
+  #e10s-button[cui-areatype="toolbar"] {
+    -moz-image-region: rect(0, 684px, 36px, 648px);
+  }
+
+  #e10s-button[cui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
+    -moz-image-region: rect(36px, 684px, 72px, 648px);
+  }
+
+  #e10s-button > .toolbarbutton-icon {
+    transform: scaleY(-1);
+  }
+
   #new-tab-button[cui-areatype="toolbar"] {
     -moz-image-region: rect(0, 720px, 36px, 684px);
   }
 
   #new-tab-button[cui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 720px, 72px, 684px);
   }
 
@@ -1185,16 +1201,21 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     -moz-image-region: rect(64px, 960px, 128px, 896px);
   }
 
   #new-window-button[cui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #new-window-button {
     -moz-image-region: rect(0px, 1024px, 64px, 960px);
   }
 
+  #e10s-button[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > #e10s-button {
+    -moz-image-region: rect(0px, 1024px, 64px, 960px);
+  }
+
   #new-tab-button[cui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #new-tab-button {
     -moz-image-region: rect(0px, 1088px, 64px, 1024px);
   }
 
   #privatebrowsing-button[cui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #privatebrowsing-button {
     -moz-image-region: rect(0px, 1152px, 64px, 1088px);
--- a/browser/themes/osx/preferences/in-content/preferences.css
+++ b/browser/themes/osx/preferences/in-content/preferences.css
@@ -7,17 +7,16 @@
 menulist:not([editable="true"]) > .menulist-dropmarker {
   display: -moz-box;
   margin-top: 1px;
   margin-bottom: 1px;
 }
 
 checkbox:hover::before,
 checkbox[checked]::before {
-  margin-bottom: -2px;
   -moz-margin-end: -20px;
   -moz-margin-start: 5px;
 }
 
 radio:hover::before,
 radio[selected]::before {
   -moz-margin-end: -18px;
   -moz-margin-start: 7px;
--- a/browser/themes/shared/browser.inc
+++ b/browser/themes/shared/browser.inc
@@ -1,13 +1,13 @@
 %filter substitution
 
 % Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none.
 %define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
-%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@
+%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button
 
 %ifdef XP_MACOSX
 % Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen
 % and want it to behave like other toolbar buttons.
 %define primaryToolbarButtons @primaryToolbarButtons@, #restore-button
 %endif
 
 %define inAnyPanel :-moz-any(:not([cui-areatype="toolbar"]), [overflowedItem=true])
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -13,39 +13,45 @@ a {
 
 /* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
  * assertion when loading HTML page with links in XUL iframe */
 *:visited { }
 
 .message {
   display: flex;
   flex: 0 0 auto;
-  -moz-margin-start: 6px;
-  -moz-margin-end: 8px;
-  width: calc(100% - 6px - 8px);
+  padding: 0 7px;
+  width: 100%;
+  box-sizing: border-box;
 }
 
 .message > .timestamp {
   flex: 0 0 auto;
   color: GrayText;
-  margin: 4px 0;
+  margin: 4px 6px 0 0;
+}
+
+.message > .indent {
+  flex: 0 0 auto;
 }
 
 .message > .icon {
   background: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 1, 0, 0) no-repeat;
-  background-position: center 0.5em;
+  background-position: center;
   flex: 0 0 auto;
-  margin: 0 6px;
+  margin: 3px 6px 0 0;
   padding: 0 4px;
   width: 8px;
+  height: 1em;
+  align-self: flex-start;
 }
 
 .message > .message-body-wrapper {
   flex: 1 1 100%;
-  margin-top: 4px;
+  margin: 3px;
 }
 
 /* The red bubble that shows the number of times a message is repeated */
 .message-repeats {
   -moz-user-select: none;
   flex: 0 0 auto;
   margin: 2px 6px;
   padding: 0 6px;
@@ -58,23 +64,22 @@ a {
   font-weight: 600;
 }
 
 .message-repeats[value="1"] {
   display: none;
 }
 
 .message-location {
-  -moz-margin-start: 6px;
   display: flex;
   flex: 0 0 auto;
   align-self: flex-start;
   justify-content: flex-end;
   width: 10em;
-  margin: 3px 0;
+  margin-top: 3px;
   color: -moz-nativehyperlinktext;
   text-decoration: none;
   white-space: nowrap;
 }
 
 .message-location:hover,
 .message-location:focus {
   text-decoration: underline;
@@ -97,16 +102,21 @@ a {
 .message-body {
   white-space: pre-wrap;
   word-wrap: break-word;
 }
 
 .message-flex-body > .message-body {
   display: block;
   flex: 1 1 auto;
+  vertical-align: middle;
+}
+
+.message-flex-body > .message-location {
+  margin-top: 0;
 }
 
 .jsterm-input-container {
   border-top-width: 1px;
   border-top-style: solid;
 }
 
 #output-wrapper {
@@ -117,20 +127,34 @@ a {
 #output-container {
   -moz-user-select: text;
   -moz-box-flex: 1;
   display: flex;
   flex-direction: column;
   align-items: flex-start;
 }
 
+#output-container.hideTimestamps > .message {
+  -moz-padding-start: 0;
+  -moz-margin-start: 7px;
+  width: calc(100% - 7px);
+}
+
 #output-container.hideTimestamps > .message > .timestamp {
   display: none;
 }
 
+.theme-dark #output-container.hideTimestamps > .message > .indent {
+  background-color: #14171a; /* .theme-body */
+}
+
+.theme-light #output-container.hideTimestamps > .message > .indent {
+  background-color: #fcfcfc; /* .theme-body */
+}
+
 .filtered-by-type,
 .filtered-by-string {
   display: none;
 }
 
 .hidden-message {
   display: block;
   visibility: hidden;
@@ -165,21 +189,29 @@ a {
   border-color: #777;
 }
 
 .theme-light .message[severity=error] {
   background-color: rgba(255, 150, 150, 0.3);
 }
 
 .theme-dark .message[severity=error] {
-  background-color: rgba(255, 100, 100, 0.3);
+  background-color: rgba(235, 83, 104, 0.17);
+}
+
+.theme-dark .console-string {
+  color: #d99b28;
 }
 
-.message[category=network] > .icon {
-  -moz-border-start: solid #000 6px;
+.theme-light .console-string {
+  color: hsl(24,85%,39%);
+}
+
+.message[category=network] > .indent {
+  -moz-border-end: solid #000 6px;
 }
 
 .message[category=network][severity=error] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 16, 8, 8);
 }
 
 .message[category=network] > .message-body {
   display: flex;
@@ -214,73 +246,73 @@ a {
 }
 
 /* CSS styles */
 .webconsole-filter-button[category="css"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#2DC3F3, #00B6F0);
   border-color: #1BA2CC;
 }
 
-.message[category=cssparser] > .icon {
-  -moz-border-start: solid #00b6f0 6px;
+.message[category=cssparser] > .indent {
+  -moz-border-end: solid #00b6f0 6px;
 }
 
 .message[category=cssparser][severity=error] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 8, 16, 16, 8);
 }
 
 .message[category=cssparser][severity=warn] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 8, 24, 16, 16);
 }
 
 /* JS styles */
 .webconsole-filter-button[category="js"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#FCB142, #FB9500);
   border-color: #E98A00;
 }
 
-.message[category=exception] > .icon {
-  -moz-border-start: solid #fb9500 6px;
+.message[category=exception] > .indent {
+  -moz-border-end: solid #fb9500 6px;
 }
 
 .message[category=exception][severity=error] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 16, 16, 24, 8);
 }
 
 .message[category=exception][severity=warn] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 16, 24, 24, 16);
 }
 
 /* Web Developer styles */
 .webconsole-filter-button[category="logging"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#B9B9B9, #AAAAAA);
   border-color: #929292;
 }
 
-.message[category=console] > .icon {
-  -moz-border-start: solid #cbcbcb 6px;
+.message[category=console] > .indent {
+  -moz-border-end: solid #cbcbcb 6px;
 }
 
 .message[category=console][severity=error] > .icon,
 .message[category=output][severity=error] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 16, 32, 8);
 }
 
 .message[category=console][severity=warn] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 24, 32, 16);
 }
 
 .message[category=console][severity=info] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 32, 32, 24);
 }
 
 /* Input and output styles */
-.message[category=input] > .icon,
-.message[category=output] > .icon {
-  -moz-border-start: solid #808080 6px;
+.message[category=input] > .indent,
+.message[category=output] > .indent {
+  -moz-border-end: solid #808080 6px;
 }
 
 .message[category=input] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 40, 32, 32);
 }
 
 .message[category=output] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 48, 32, 40);
@@ -324,18 +356,18 @@ a {
 }
 
 .devtools-side-splitter ~ #webconsole-sidebar[hidden] {
   display: none;
 }
 
 /* Security styles */
 
-.message[category=security] > .icon {
-  -moz-border-start: solid red 6px;
+.message[category=security] > .indent {
+  -moz-border-end: solid red 6px;
 }
 
 .webconsole-filter-button[category="security"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#FF3030, #FF7D7D);
   border-color: #D12C2C;
 }
 
 .message[category=security][severity=error] > .icon {
@@ -381,17 +413,17 @@ a {
 
 .message[open] .stacktrace {
   display: block;
 }
 
 .message .theme-twisty {
   display: inline-block;
   vertical-align: middle;
-  margin: 2px 3px 0 0;
+  margin: 0 3px 0 0;
 }
 
 .stacktrace li {
   display: flex;
   margin: 0;
 }
 
 .stacktrace .function {
--- a/browser/themes/shared/incontentprefs/preferences.css
+++ b/browser/themes/shared/incontentprefs/preferences.css
@@ -337,17 +337,18 @@ html|a.inline-link {
 .inline-link:hover:active {
   color: #FF9500;
   text-decoration: none;
 }
 
 /* Checkboxes and radio buttons */
 
 checkbox {
-  margin: 7px 0;
+  -moz-margin-start: 0;
+  position: relative;
 }
 
 .checkbox-check {
   -moz-appearance: none;
   width: 23px;
   height: 23px;
   border-radius: 2px;
   border: 1px solid rgba(23,50,77,0.40);
@@ -410,24 +411,27 @@ checkbox[checked]::before {
 }
 
 .radio-label-box {
   -moz-margin-start: -1px; /* negative margin for the transparent border */
   -moz-margin-end: 10px;
   -moz-padding-start: 0;
 }
 
+radio {
+  position: relative;
+}
+
 radio:hover::before,
 radio[selected]::before {
   position: absolute;
   content: "";
   width: 11px;
   height: 11px;
   border-radius: 50%;
-  margin-bottom: 1px;
   background-image: linear-gradient(rgba(76,177,255,0.25) 0%, rgba(23,146,229,0.25) 100%);
 }
 
 radio[selected]::before {
   background-image: linear-gradient(#4cb1ff 0%, #1792e5 100%);
 }
 
 /* Category List */
@@ -675,21 +679,16 @@ radio[selected]::before {
     -moz-image-region: rect(0, 480px, 80px, 400px);
   }
 
   #header-advanced > .header-icon {
     -moz-image-region: rect(0, 560px, 80px, 480px);
   }
 }
 
-.indent {
-  margin-top: 7px;
-  margin-bottom: 7px;
-}
-
 /* General Pane */
 
 filefield {
   -moz-appearance: none;
   background-color: transparent;
   border: none;
   padding: 0;
 }
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -78,16 +78,21 @@ toolbarpaletteitem[place="palette"] > #c
   -moz-image-region: rect(32px, 480px, 64px, 448px);
 }
 
 #new-window-button[cui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #new-window-button {
   -moz-image-region: rect(0px, 512px, 32px, 480px);
 }
 
+#e10s-button[cui-areatype="menu-panel"],
+toolbarpaletteitem[place="palette"] > #e10s-button {
+  -moz-image-region: rect(0px, 512px, 32px, 480px);
+}
+
 #new-tab-button[cui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #new-tab-button {
   -moz-image-region: rect(0px, 544px, 32px, 512px);
 }
 
 #privatebrowsing-button[cui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #privatebrowsing-button {
   -moz-image-region: rect(0px, 576px, 32px, 544px);
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -80,16 +80,24 @@
 #characterencoding-button[cui-areatype="toolbar"]{
   -moz-image-region: rect(0, 324px, 18px, 306px);
 }
 
 #new-window-button[cui-areatype="toolbar"] {
   -moz-image-region: rect(0, 342px, 18px, 324px);
 }
 
+#e10s-button[cui-areatype="toolbar"] {
+  -moz-image-region: rect(0, 342px, 18px, 324px);
+}
+
+#e10s-button > .toolbarbutton-icon {
+  transform: scaleY(-1);
+}
+
 #new-tab-button[cui-areatype="toolbar"] {
   -moz-image-region: rect(0, 360px, 18px, 342px);
 }
 
 #privatebrowsing-button[cui-areatype="toolbar"] {
   -moz-image-region: rect(0, 378px, 18px, 360px);
 }
 
--- a/browser/themes/windows/preferences/in-content/preferences.css
+++ b/browser/themes/windows/preferences/in-content/preferences.css
@@ -6,24 +6,23 @@
 
 menulist:not([editable="true"]) > .menulist-dropmarker {
   margin-top: 1px;
   margin-bottom: 1px;
 }
 
 checkbox:hover::before,
 checkbox[checked]::before {
-  margin-bottom: -1px;
   -moz-margin-end: -19px;
   -moz-margin-start: 4px;
 }
 
 radio {
   -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
-  margin: 7px 0;
+  -moz-margin-start: 0;
 }
 
 radio:hover::before,
 radio[selected]::before {
   -moz-margin-end: -17px;
   -moz-margin-start: 6px;
 }
 
--- a/mobile/android/base/EventDispatcher.java
+++ b/mobile/android/base/EventDispatcher.java
@@ -1,16 +1,17 @@
 /* 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/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSContainer;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -203,16 +204,17 @@ public final class EventDispatcher {
             for (final GeckoEventListener listener : listeners) {
                 listener.handleMessage(type, message);
             }
         } catch (final JSONException e) {
             Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
         }
     }
 
+    @RobocopTarget
     @Deprecated
     public static void sendResponse(JSONObject message, Object response) {
         sendResponseHelper(STATUS_SUCCESS, message, response);
     }
 
     @Deprecated
     public static void sendError(JSONObject message, Object response) {
         sendResponseHelper(STATUS_ERROR, message, response);
--- a/mobile/android/base/TextSelection.java
+++ b/mobile/android/base/TextSelection.java
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.ActionModeCompat.Callback;
 
@@ -35,16 +36,19 @@ import android.view.View;
 class TextSelection extends Layer implements GeckoEventListener {
     private static final String LOGTAG = "GeckoTextSelection";
 
     private final TextSelectionHandle mStartHandle;
     private final TextSelectionHandle mMiddleHandle;
     private final TextSelectionHandle mEndHandle;
     private final EventDispatcher mEventDispatcher;
 
+    private final DrawListener mDrawListener;
+    private boolean mDraggingHandles;
+
     private float mViewLeft;
     private float mViewTop;
     private float mViewZoom;
 
     private String mCurrentItems;
 
     private TextSelectionActionModeCallback mCallback;
 
@@ -69,78 +73,95 @@ class TextSelection extends Layer implem
                   TextSelectionHandle endHandle,
                   EventDispatcher eventDispatcher,
                   GeckoApp activity) {
         mStartHandle = startHandle;
         mMiddleHandle = middleHandle;
         mEndHandle = endHandle;
         mEventDispatcher = eventDispatcher;
 
+        mDrawListener = new DrawListener() {
+            @Override
+            public void drawFinished() {
+                if (!mDraggingHandles) {
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", ""));
+                }
+            }
+        };
+
         // Only register listeners if we have valid start/middle/end handles
         if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) {
             Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
         } else {
             registerEventListener("TextSelection:ShowHandles");
             registerEventListener("TextSelection:HideHandles");
             registerEventListener("TextSelection:PositionHandles");
             registerEventListener("TextSelection:Update");
+            registerEventListener("TextSelection:DraggingHandle");
         }
     }
 
     void destroy() {
         unregisterEventListener("TextSelection:ShowHandles");
         unregisterEventListener("TextSelection:HideHandles");
         unregisterEventListener("TextSelection:PositionHandles");
         unregisterEventListener("TextSelection:Update");
+        unregisterEventListener("TextSelection:DraggingHandle");
     }
 
     private TextSelectionHandle getHandle(String name) {
         if (name.equals("START")) {
             return mStartHandle;
         } else if (name.equals("MIDDLE")) {
             return mMiddleHandle;
         } else {
             return mEndHandle;
         }
     }
 
     @Override
     public void handleMessage(final String event, final JSONObject message) {
+        if ("TextSelection:DraggingHandle".equals(event)) {
+            mDraggingHandles = message.optBoolean("dragging", false);
+            return;
+        }
+
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 try {
                     if (event.equals("TextSelection:ShowHandles")) {
                         final JSONArray handles = message.getJSONArray("handles");
                         for (int i=0; i < handles.length(); i++) {
                             String handle = handles.getString(i);
                             getHandle(handle).setVisibility(View.VISIBLE);
                         }
 
                         mViewLeft = 0.0f;
                         mViewTop = 0.0f;
                         mViewZoom = 0.0f;
+
+                        // Create text selection layer and add draw-listener for positioning on reflows
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
+                            layerView.addDrawListener(mDrawListener);
                             layerView.addLayer(TextSelection.this);
                         }
 
-                        if (mActionModeTimerTask != null)
-                            mActionModeTimerTask.cancel();
-                        showActionMode(message.getJSONArray("actions"));
-
                         if (handles.length() > 1)
                             GeckoAppShell.performHapticFeedback(true);
                     } else if (event.equals("TextSelection:Update")) {
                         if (mActionModeTimerTask != null)
                             mActionModeTimerTask.cancel();
                         showActionMode(message.getJSONArray("actions"));
                     } else if (event.equals("TextSelection:HideHandles")) {
+                        // Remove draw-listener and text selection layer
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
+                            layerView.removeDrawListener(mDrawListener);
                             layerView.removeLayer(TextSelection.this);
                         }
 
                         mActionModeTimerTask = new ActionModeTimerTask();
                         mActionModeTimer.schedule(mActionModeTimerTask, 250);
 
                         mStartHandle.setVisibility(View.GONE);
                         mMiddleHandle.setVisibility(View.GONE);
--- a/mobile/android/base/favicons/cache/FaviconCache.java
+++ b/mobile/android/base/favicons/cache/FaviconCache.java
@@ -412,17 +412,16 @@ public class FaviconCache {
             FaviconsForURL element = permanentBackingMap.get(key);
             if (element == null) {
                 element = backingMap.get(key);
             }
 
             if (element == null) {
                 Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon. Cache fullness " +
                               currentSize.get() + '/' + maxSizeBytes);
-                finishRead();
                 return 0xFFFFFF;
             }
 
             return element.ensureDominantColor();
         } finally {
             finishRead();
         }
     }
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -87,16 +87,17 @@ public class StringHelper {
     public static final String ROBOCOP_BOXES_TITLE = "Browser Box test";
     public static final String ROBOCOP_GEOLOCATION_TITLE = "Geolocation Test Page";
     public static final String ROBOCOP_LOGIN_TITLE = "Robocop Login";
     public static final String ROBOCOP_OFFLINE_STORAGE_TITLE = "Robocop offline storage";
     public static final String ROBOCOP_PICTURE_LINK_TITLE = "Picture Link";
     public static final String ROBOCOP_SEARCH_TITLE = "Robocop Search Engine";
     public static final String ROBOCOP_TEXT_PAGE_TITLE = "Robocop Text Page";
     public static final String ROBOCOP_INPUT_TITLE = "Robocop Input";
+    public static final String ROBOCOP_SELECTION_HANDLER_TITLE = "Automated Text Selection tests for Mobile";
 
     // Settings menu strings
     // Section labels - ordered as found in the settings menu
     public static final String CUSTOMIZE_SECTION_LABEL = "Customize";
     public static final String DISPLAY_SECTION_LABEL = "Display";
     public static final String PRIVACY_SECTION_LABEL = "Privacy";
     public static final String MOZILLA_SECTION_LABEL = "Mozilla";
     public static final String DEVELOPER_TOOLS_SECTION_LABEL = "Developer tools";
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -76,16 +76,17 @@ skip-if = processor == "x86"
 [testPasswordProvider]
 # [testPermissions] # see bug 757475
 [testPictureLinkContextMenu]
 # disabled on 2.3; bug 986164
 skip-if = android_version == "10"
 [testPrefsObserver]
 [testPrivateBrowsing]
 [testPromptGridInput]
+[testSelectionHandler]
 # disabled on x86 only; bug 957185
 skip-if = processor == "x86"
 # [testReaderMode] # see bug 913254, 936224
 [testReadingListProvider]
 [testSearchSuggestions]
 # disabled on x86 and 2.3; bug 907768, 979548
 skip-if = android_version == "10" || processor == "x86"
 [testSessionOOMSave]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -0,0 +1,301 @@
+<html>
+  <head>
+    <title>Automated Text Selection tests for Mobile</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">
+
+const DIV_POINT_TEXT = "Under";
+const INPUT_TEXT = "Text for select all in an <input>";
+const TEXTAREA_TEXT = "Text for select all in a <textarea>";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/* =================================================================================
+ *
+ * Start of all text selection tests, check initialization state.
+ *
+ */
+function startTests() {
+  testSelectAllDivs().
+    then(testSelectDivAtPoint).
+    then(testSelectInput).
+    then(testSelectTextarea).
+    then(testCloseSelection).
+    then(finishTests, function(err) {
+      ok(false, "Error in selection test " + err);
+      finishTests();
+    });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection tests, for <div> (non-editable) fields.
+ *
+ */
+function testSelectAllDivs() {
+  var sh = getSelectionHandler();
+  var selDiv = document.getElementById("selDiv");
+  var nonSelDiv = document.getElementById("nonSelDiv");
+
+  // 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 selection = sh._getSelection();
+
+    return Promise.all([
+      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"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select word-at-point" text selection test, for <div> (non-editable) field.
+ * "collapseToStart" test closes selection (Bug 864589).
+ *
+ */
+function testSelectDivAtPoint() {
+  var sh = getSelectionHandler();
+  var selDiv = document.getElementById("selDiv");
+
+  // Select word at point in <div>
+  var rect = selDiv.getBoundingClientRect();
+  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(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(!sh.isSelectionActive(), "Selection should not be active"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection test, for <input> (editable) field.
+ *
+ */
+function testSelectInput() {
+  var sh = getSelectionHandler();
+  var inputNode = document.getElementById("inputNode");
+  inputNode.value = INPUT_TEXT;
+
+  // 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 selection = sh._getSelection();
+
+    return Promise.all([
+      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"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * "Select all" text selection test, for <textarea> (editable) field.
+ *
+ */
+
+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 selection = sh._getSelection();
+
+  return Promise.all([
+    ok(sh.isSelectionActive(), "Selection should be active at start of testSelectTextarea"),
+    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([
+      ok(!sh.isSelectionActive(), "Selection should not be active"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
+ * Various text selection tests to end active selections, including:
+ *   1.) Clicking outside the selection.
+ *   2.) SelectionEnd or Tab:Selected messages from java.
+ *
+ */
+function testCloseSelection() {
+  var sh = getSelectionHandler();
+  var inputNode = document.getElementById("inputNode");
+  inputNode.value = INPUT_TEXT;
+
+  // Check the initial state of the selection handler.
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testCloseSelection"),
+
+  ]).then(function() {
+    // Start by selecting all in an <input>.
+    sh.startSelection(inputNode);
+    return is(sh._activeType, sh.TYPE_SELECTION, "Selection should be active in <input> before Gesture:SingleTap");
+
+  }).then(function() {
+    // Tap outside <input> to close active selection.
+    sh.observe(null, "Gesture:SingleTap", JSON.stringify({
+      x: 1,
+      y: 1
+    }));
+    return ok(!sh.isSelectionActive(), "Gesture:SingleTap outside <input> should close active selection");
+
+  // Various other ways to close an active selection.
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.observe(null, "TextSelection:End", {});
+    return ok(!sh.isSelectionActive(), "TextSelection:End should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.observe(null, "Tab:Selected", {});
+    return ok(!sh.isSelectionActive(), "Tab:Selected should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.handleEvent({ type: "pagehide" });
+    return ok(!sh.isSelectionActive(), "pagehide should close active selection");
+
+  }).then(function() {
+    sh.startSelection(inputNode);
+    sh.handleEvent({ type: "blur" });
+    return ok(!sh.isSelectionActive(), "blur should close active selection");
+  });
+}
+
+/* =================================================================================
+ *
+ * After finish of all selection tests, wrap up and go home.
+ *
+ */
+function finishTests() {
+  sendMessageToJava({
+    type: "Robocop:testSelectionHandler",
+    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 ok(one, msg) {
+  return new Promise(function(resolve, reject) {
+    sendMessageToJava({
+      type: "Robocop:testSelectionHandler",
+      result: one,
+      msg: msg
+    },
+    function (res, err) {
+      (err) ? reject(err) : resolve(res);
+    });
+  });
+}
+
+function is(one, two, msg) {
+  return new Promise(function(resolve, reject) {
+    sendMessageToJava({
+      type: "Robocop:testSelectionHandler",
+      result: one === two,
+      msg: msg + " : " + one + " === " + two
+    },
+    function (res, err) {
+      (err) ? reject(err) : resolve(res);
+    });
+  });
+}
+
+/* =================================================================================
+ *
+ * Page definition for all tests.
+ *
+ */
+    </script>
+  </head>
+
+  <body onload="startTests();">
+
+    <div id="selDiv">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. A recent estimate puts the needed temperature at
+      (1.90±0.02)×1012 Kelvin. While a state of entirely free quarks and gluons has
+      never been achieved (despite numerous attempts by CERN in the 1980s and 1990s),
+      recent experiments at the Relativistic Heavy Ion Collider have yielded evidence
+      for liquid-like quark matter exhibiting "nearly perfect" fluid motion.</div><br>
+
+    <div id="nonSelDiv" style="-moz-user-select: none;">Lorem ipsum dolor sit amet,
+      consectetur adipiscing elit. Proin in blandit magna, non porttitor augue.
+      Nam in neque sagittis, varius augue at, ornare velit. Vestibulum eget nisl
+      congue odio molestie scelerisque. Pellentesque ut augue orci. In hac habitasse
+      platea dictumst. Sed placerat tellus quis lacus condimentum, quis luctus elit
+      pellentesque. Mauris cursus neque diam, sit amet gravida quam porta ac.
+      Aliquam aliquam feugiat vestibulum. Proin commodo nulla ligula, in bibendum
+      massa euismod a. Ut ac lobortis dui. Ut id augue id arcu ornare suscipit eu
+      ornare lorem. Pellentesque nec dictum ante. Nam quis ligula ultricies, auctor
+      nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus
+      ligula interdum enim, vel varius libero sem ut ligula.</div><br>
+
+    <input id="inputNode" type="text"><br>
+
+    <textarea id="textareaNode"></textarea><br>
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testSelectionHandler.java
@@ -0,0 +1,46 @@
+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 testSelectionHandler extends UITest {
+
+    public void testSelectionHandler() {
+        GeckoHelper.blockForReady();
+
+        Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent("Robocop:testSelectionHandler");
+        NavigationHelper.enterAndLoadUrl("chrome://roboextender/content/testSelectionHandler.html");
+        mToolbar.assertTitle(StringHelper.ROBOCOP_SELECTION_HANDLER_TITLE);
+
+        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"));
+        }
+
+        EventDispatcher.sendResponse(eventData, new JSONObject());
+        return eventData.optBoolean("done", false);
+    }
+}
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -17,16 +17,20 @@ var SelectionHandler = {
   SELECT_AT_POINT: 1,
 
   // 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
+
+  // 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();
     return null;
   },
 
@@ -54,38 +58,52 @@ var SelectionHandler = {
   _addObservers: function sh_addObservers() {
     Services.obs.addObserver(this, "Gesture:SingleTap", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "after-viewport-change", false);
     Services.obs.addObserver(this, "TextSelection:Move", false);
     Services.obs.addObserver(this, "TextSelection:Position", false);
     Services.obs.addObserver(this, "TextSelection:End", false);
     Services.obs.addObserver(this, "TextSelection:Action", false);
+    Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
 
     BrowserApp.deck.addEventListener("pagehide", this, false);
     BrowserApp.deck.addEventListener("blur", this, true);
     BrowserApp.deck.addEventListener("scroll", this, true);
   },
 
   _removeObservers: function sh_removeObservers() {
     Services.obs.removeObserver(this, "Gesture:SingleTap");
     Services.obs.removeObserver(this, "Tab:Selected");
     Services.obs.removeObserver(this, "after-viewport-change");
     Services.obs.removeObserver(this, "TextSelection:Move");
     Services.obs.removeObserver(this, "TextSelection:Position");
     Services.obs.removeObserver(this, "TextSelection:End");
     Services.obs.removeObserver(this, "TextSelection:Action");
+    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) {
     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) {
+          this._positionHandlesOnChange();
+        }
+        break;
+      }
+
       // Update caret position on keyboard activity
       case "TextSelection:UpdateCaretPos":
         // Generated by IME close, autoCorrection / styling
         this._positionHandles();
         break;
 
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
@@ -120,16 +138,18 @@ var SelectionHandler = {
       }
       case "TextSelection:Move": {
         let data = JSON.parse(aData);
         if (this._activeType == this.TYPE_SELECTION) {
           this._startDraggingHandles();
           this._moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
 
         } else if (this._activeType == this.TYPE_CURSOR) {
+          this._startDraggingHandles();
+
           // Ignore IMM composition notifications when caret movement starts
           this._ignoreCompositionChanges = true;
 
           // Send a click event to the text box, which positions the caret
           this._sendMouseEvents(data.x, data.y);
 
           // Move the handle directly under the caret
           this._positionHandles();
@@ -155,20 +175,23 @@ var SelectionHandler = {
           } catch (e) {
             // User finished handle positioning with one end off the screen
             this._closeSelection();
             break;
           }
 
           this._stopDraggingHandles();
           this._positionHandles();
+          // Changes to handle position can affect selection context and actionbar display
+          this._updateMenu();
 
         } else if (this._activeType == this.TYPE_CURSOR) {
           // Act on IMM composition notifications after caret movement ends
           this._ignoreCompositionChanges = false;
+          this._stopDraggingHandles();
           this._positionHandles();
 
         } else {
           Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
         }
 
         break;
       }
@@ -200,17 +223,17 @@ var SelectionHandler = {
       sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: false });
     }
   },
 
   handleEvent: function sh_handleEvent(aEvent) {
     switch (aEvent.type) {
       case "scroll":
         // Maintain position when top-level document is scrolled
-        this._positionHandles();
+        this._positionHandlesOnChange();
         break;
 
       case "pagehide":
       case "blur":
         this._closeSelection();
         break;
 
       // Update caret position on keyboard activity
@@ -349,18 +372,23 @@ var SelectionHandler = {
 
     if (aOptions.mode == this.SELECT_AT_POINT && !this._selectionNearClick(scroll.X + aOptions.x,
                                                                       scroll.Y + aOptions.y,
                                                                       positions)) {
         this._closeSelection();
         return false;
     }
 
+    // Determine position and show handles, open actionbar
     this._positionHandles(positions);
-    this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]);
+    sendMessageToJava({
+      type: "TextSelection:ShowHandles",
+      handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
+    });
+    this._updateMenu();
     return true;
   },
 
   /*
    * 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) {
@@ -428,17 +456,41 @@ var SelectionHandler = {
       return defaultValue;
 
     if (typeof obj[name] == "function")
       return obj[name](this._targetElement);
 
     return obj[name];
   },
 
-  _sendMessage: function(msgType, handles) {
+  addAction: function(action) {
+    if (!action.id)
+      action.id = uuidgen.generateUUID().toString()
+
+    if (this.actions[action.id])
+      throw "Action with id " + action.id + " already added";
+
+    // Update actions list and actionbar UI if active.
+    this.actions[action.id] = action;
+    this._updateMenu();
+    return action.id;
+  },
+
+  removeAction: function(id) {
+    // Update actions list and actionbar UI if active.
+    delete this.actions[id];
+    this._updateMenu();
+  },
+
+  _updateMenu: function() {
+    if (this._activeType == this.TYPE_NONE) {
+      return;
+    }
+
+    // Update actionbar UI.
     let actions = [];
     for (let type in this.actions) {
       let action = this.actions[type];
       if (action.selector.matches(this._targetElement)) {
         let a = {
           id: action.id,
           label: this._getValue(action, "label", ""),
           icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
@@ -447,41 +499,21 @@ var SelectionHandler = {
         };
         actions.push(a);
       }
     }
 
     actions.sort((a, b) => b.order - a.order);
 
     sendMessageToJava({
-      type: msgType,
-      handles: handles,
-      actions: actions,
+      type: "TextSelection:Update",
+      actions: actions
     });
   },
 
-  _updateMenu: function() {
-    this._sendMessage("TextSelection:Update");
-  },
-
-  addAction: function(action) {
-    if (!action.id)
-      action.id = uuidgen.generateUUID().toString()
-
-    if (this.actions[action.id])
-      throw "Action with id " + action.id + " already added";
-
-    this.actions[action.id] = action;
-    return action.id;
-  },
-
-  removeAction: function(id) {
-    delete this.actions[id];
-  },
-
   /*
    * Actionbar methods.
    */
   actions: {
     SELECT_ALL: {
       label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
       id: "selectall_action",
       icon: "drawable://ab_select_all",
@@ -627,19 +659,24 @@ var SelectionHandler = {
 
     // Caret-specific observer/listeners
     Services.obs.addObserver(this, "TextSelection:UpdateCaretPos", false);
     BrowserApp.deck.addEventListener("keyup", this, false);
     BrowserApp.deck.addEventListener("compositionupdate", this, false);
     BrowserApp.deck.addEventListener("compositionend", this, false);
 
     this._activeType = this.TYPE_CURSOR;
+
+    // Determine position and show caret, open actionbar
     this._positionHandles();
-
-    this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_MIDDLE]);
+    sendMessageToJava({
+      type: "TextSelection:ShowHandles",
+      handles: [this.HANDLE_TYPE_MIDDLE]
+    });
+    this._updateMenu();
   },
 
   // 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
@@ -900,16 +937,17 @@ var SelectionHandler = {
       if (selection.rangeCount != 0) {
         selection.collapseToStart();
       }
     }
   },
 
   _deactivate: function sh_deactivate() {
     this._stopDraggingHandles();
+    // Hide handle/caret, close actionbar
     sendMessageToJava({ type: "TextSelection:HideHandles" });
 
     this._removeObservers();
 
     // Only observed for caret positioning
     if (this._activeType == this.TYPE_CURSOR) {
       Services.obs.removeObserver(this, "TextSelection:UpdateCaretPos");
       BrowserApp.deck.removeEventListener("keyup", this);
@@ -917,16 +955,18 @@ var SelectionHandler = {
       BrowserApp.deck.removeEventListener("compositionend", this);
     }
 
     this._contentWindow = null;
     this._targetElement = null;
     this._isRTL = false;
     this._cache = null;
     this._ignoreCompositionChanges = false;
+    this._prevHandlePositions = [];
+    this._prevTargetElementHasText = null;
 
     this._activeType = this.TYPE_NONE;
   },
 
   _getViewOffset: function sh_getViewOffset() {
     let offset = { x: 0, y: 0 };
     let win = this._contentWindow;
 
@@ -1021,28 +1061,61 @@ var SelectionHandler = {
                  hidden: checkHidden(sx, sy) },
                { handle: this.HANDLE_TYPE_END,
                  left: ex + offset.x + scroll.X,
                  top: ey + offset.y + scroll.Y,
                  hidden: checkHidden(ex, ey) }];
     }
   },
 
+  // Position handles, but avoid superfluous re-positioning (helps during
+  // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
+  _positionHandlesOnChange: function() {
+    // Helper function to compare position messages
+    let samePositions = function(aPrev, aCurr) {
+      if (aPrev.length != aCurr.length) {
+        return false;
+      }
+      for (let i = 0; i < aPrev.length; i++) {
+        if (aPrev[i].left != aCurr[i].left ||
+            aPrev[i].top != aCurr[i].top ||
+            aPrev[i].hidden != aCurr[i].hidden) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    let positions = this._getHandlePositions(this._getScrollPos());
+    if (!samePositions(this._prevHandlePositions, positions)) {
+      this._positionHandles(positions);
+    }
+  },
+
+  // Position handles, allow for re-position, in case user drags handle
+  // to invalid position, then releases, we can put it back where it started
   // positions is an array of objects with data about handle positions,
   // which we get from _getHandlePositions.
   _positionHandles: function sh_positionHandles(positions) {
     if (!positions) {
       positions = this._getHandlePositions(this._getScrollPos());
     }
     sendMessageToJava({
       type: "TextSelection:PositionHandles",
       positions: positions,
       rtl: this._isRTL
     });
-    this._updateMenu();
+    this._prevHandlePositions = positions;
+
+    // Text state transitions (text <--> no text) will affect selection context and actionbar display
+    let currTargetElementHasText = (this._targetElement.textLength > 0);
+    if (currTargetElementHasText != this._prevTargetElementHasText) {
+      this._prevTargetElementHasText = currTargetElementHasText;
+      this._updateMenu();
+    }
   },
 
   subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
     if (this._activeType == this.TYPE_NONE) {
       return;
     }
     let scrollView = aElement.ownerDocument.defaultView;
     let view = this._contentWindow;
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -233,27 +233,31 @@ let HomePanels = (function () {
         throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
             + ", view.index = " + data.viewIndex;
       }
 
       view.onrefresh();
     },
 
     "HomePanels:Installed": function handlePanelsInstalled(id) {
+      _assertPanelExists(id);
+
       let options = _registeredPanels[id]();
       if (!options.oninstall) {
         return;
       }
       if (typeof options.oninstall !== "function") {
         throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
       }
       options.oninstall();
     },
 
     "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
+      _assertPanelExists(id);
+
       let options = _registeredPanels[id]();
       if (!options.onuninstall) {
         return;
       }
       if (typeof options.onuninstall !== "function") {
         throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
       }
       options.onuninstall();
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -271,16 +271,34 @@ DevToolsLoader.prototype = {
    * @see setProvider
    */
   require: function() {
     this._chooseProvider();
     return this.require.apply(this, arguments);
   },
 
   /**
+   * Define a getter property on the given object that requires the given
+   * module. This enables delaying importing modules until the module is
+   * actually used.
+   *
+   * @param Object obj
+   *    The object to define the property on.
+   * @param String property
+   *    The property name.
+   * @param String module
+   *    The module path.
+   */
+  lazyRequireGetter: function (obj, property, module) {
+    Object.defineProperty(obj, property, {
+      get: () => this.require(module)
+    });
+  },
+
+  /**
    * Add a URI to the loader.
    * @param string id
    *    The module id that can be used within the loader to refer to this module.
    * @param string uri
    *    The URI to load as a module.
    * @returns The module's exports.
    */
   loadURI: function(id, uri) {
--- a/toolkit/devtools/tests/unit/head_devtools.js
+++ b/toolkit/devtools/tests/unit/head_devtools.js
@@ -1,14 +1,15 @@
 "use strict";
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 // Register a console listener, so console messages don't just disappear
 // into the ether.
 let errorCount = 0;
 let listener = {
   observe: function (aMessage) {
     errorCount++;
--- a/toolkit/devtools/tests/unit/test_independent_loaders.js
+++ b/toolkit/devtools/tests/unit/test_independent_loaders.js
@@ -1,14 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const { DevToolsLoader } =
-  Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-
 /**
  * Ensure that each instance of the Dev Tools loader contains its own loader
  * instance, and also returns unique objects.  This ensures there is no sharing
  * in place between loaders.
  */
 function run_test() {
   let loader1 = new DevToolsLoader();
   let loader2 = new DevToolsLoader();
--- a/toolkit/devtools/tests/unit/test_invisible_loader.js
+++ b/toolkit/devtools/tests/unit/test_invisible_loader.js
@@ -1,13 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const { DevToolsLoader } =
-  Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 addDebuggerToGlobal(this);
 
 const COLOR_URI = "resource://gre/modules/devtools/css-color.js";
 
 /**
  * Ensure that sandboxes created via the Dev Tools loader respect the
  * invisibleToDebugger flag.
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/test_require_lazy.js
@@ -0,0 +1,14 @@
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test devtools.lazyRequireGetter
+
+function run_test() {
+  const o = {};
+  devtools.lazyRequireGetter(o, "asyncUtils", "devtools/async-utils");
+  const asyncUtils = devtools.require("devtools/async-utils");
+  // XXX: do_check_eq only works on primitive types, so we have this
+  // do_check_true of an equality expression.
+  do_check_true(o.asyncUtils === asyncUtils);
+}
--- a/toolkit/devtools/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/tests/unit/xpcshell.ini
@@ -2,8 +2,9 @@
 head = head_devtools.js
 tail =
 
 [test_independent_loaders.js]
 [test_invisible_loader.js]
 [test_safeErrorString.js]
 [test_defineLazyPrototypeGetter.js]
 [test_async-utils.js]
+[test_require_lazy.js]
\ No newline at end of file
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
@@ -93,34 +93,46 @@ details.notification.enable=%1$S will be
 details.notification.disable=%1$S will be disabled after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.install) %1$S is the add-on name, %2$S is brand name
 details.notification.install=%1$S will be installed after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.uninstall) %1$S is the add-on name, %2$S is brand name
 details.notification.uninstall=%1$S will be uninstalled after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.upgrade) %1$S is the add-on name, %2$S is brand name
 details.notification.upgrade=%1$S will be updated after you restart %2$S.
 
-#LOCALIZATION NOTE (details.experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (detail view).
+# LOCALIZATION NOTE (details.experiment.time.daysRemaining):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days from now that the experiment will remain active (detail view).
 details.experiment.time.daysRemaining=#1 day remaining;#1 days remaining
 #LOCALIZATION NOTE (details.experiment.time.endsToday) The experiment will end in less than a day (detail view).
 details.experiment.time.endsToday=Less than a day remaining
-#LOCALIZATION NOTE (details.experiment.time.daysPassed) #1 is the number of days since the experiment ran (detail view).
+# LOCALIZATION NOTE (details.experiment.time.daysPassed):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days since the experiment ran (detail view).
 details.experiment.time.daysPassed=#1 day ago;#1 days ago
 #LOCALIZATION NOTE (details.experiment.time.endedToday) The experiment ended less than a day ago (detail view).
 details.experiment.time.endedToday=Less than a day ago
 #LOCALIZATION NOTE (details.experiment.state.active) This experiment is active (detail view).
 details.experiment.state.active=Active
 #LOCALIZATION NOTE (details.experiment.state.complete) This experiment is complete (it was previously active) (detail view).
 details.experiment.state.complete=Complete
 
-#LOCALIZATION NOTE (experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (list view item).
+# LOCALIZATION NOTE (experiment.time.daysRemaining):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days from now that the experiment will remain active (list view item).
 experiment.time.daysRemaining=#1 day remaining;#1 days remaining
 #LOCALIZATION NOTE (experiment.time.endsToday) The experiment will end in less than a day (list view item).
 experiment.time.endsToday=Less than a day remaining
-#LOCALIZATION NOTE (experiment.time.daysPassed) #1 is the number of days since the experiment ran (list view item).
+# LOCALIZATION NOTE (experiment.time.daysPassed):
+# Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of days since the experiment ran (list view item).
 experiment.time.daysPassed=#1 day ago;#1 days ago
 #LOCALIZATION NOTE (experiment.time.endedToday) The experiment ended less than a day ago (list view item).
 experiment.time.endedToday=Less than a day ago
 #LOCALIZATION NOTE (experiment.state.active) This experiment is active (list view item).
 experiment.state.active=Active
 #LOCALIZATION NOTE (experiment.state.complete) This experiment is complete (it was previously active) (list view item).
 experiment.state.complete=Complete