Merge mozilla-central to autoland
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Sun, 17 Jul 2016 10:11:17 +0200
changeset 330301 53900593419c215f37e70c40763ca8b45d86ecb5
parent 330300 059794b8004eba9378d28c9d584c905c40a33f04 (current diff)
parent 330297 711963e8daa312ae06409f8ab5c06612cb0b8f7b (diff)
child 330302 f22693a94979d311cda86e948e2ef58929e71932
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.0a1
Merge mozilla-central to autoland
testing/web-platform/meta/XMLHttpRequest/xmlhttprequest-network-error.htm.ini
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1267887 - Build skew after updating rust mp4parse
+Bug 1286754 - Update the version of rust used on Windows
--- a/accessible/xpcom/xpcAccessibleGeneric.h
+++ b/accessible/xpcom/xpcAccessibleGeneric.h
@@ -81,20 +81,20 @@ xpcAccessible::Intl()
 }
 
 inline AccessibleOrProxy
 xpcAccessible::IntlGeneric()
 {
   return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
 }
 
-inline Accessible*
+inline AccessibleOrProxy
 xpcAccessibleHyperLink::Intl()
 {
-  return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible();
+  return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
 }
 
 inline Accessible*
 xpcAccessibleSelectable::Intl()
 {
   return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible();
 }
 
--- a/accessible/xpcom/xpcAccessibleHyperLink.cpp
+++ b/accessible/xpcom/xpcAccessibleHyperLink.cpp
@@ -1,94 +1,156 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "Accessible-inl.h"
 #include "xpcAccessibleDocument.h"
+#include "nsNetUtil.h"
 
 using namespace mozilla::a11y;
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetStartIndex(int32_t* aStartIndex)
 {
   NS_ENSURE_ARG_POINTER(aStartIndex);
   *aStartIndex = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aStartIndex = Intl()->StartOffset();
+  if (Intl().IsAccessible()) {
+    *aStartIndex = Intl().AsAccessible()->StartOffset();
+  } else { 
+    bool isIndexValid = false;
+    uint32_t startOffset = Intl().AsProxy()->StartOffset(&isIndexValid);
+    if (!isIndexValid)
+      return NS_ERROR_FAILURE;
+
+    *aStartIndex = startOffset;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetEndIndex(int32_t* aEndIndex)
 {
   NS_ENSURE_ARG_POINTER(aEndIndex);
   *aEndIndex = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aEndIndex = Intl()->EndOffset();
+  if (Intl().IsAccessible()) {
+    *aEndIndex = Intl().AsAccessible()->EndOffset();
+  } else { 
+    bool isIndexValid = false;
+    uint32_t endOffset = Intl().AsProxy()->EndOffset(&isIndexValid);
+    if (!isIndexValid)
+      return NS_ERROR_FAILURE;
+
+    *aEndIndex = endOffset;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetAnchorCount(int32_t* aAnchorCount)
 {
   NS_ENSURE_ARG_POINTER(aAnchorCount);
   *aAnchorCount = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aAnchorCount = Intl()->AnchorCount();
+  if (Intl().IsAccessible()) {
+    *aAnchorCount = Intl().AsAccessible()->AnchorCount();
+  } else { 
+    bool isCountValid = false;
+    uint32_t anchorCount = Intl().AsProxy()->AnchorCount(&isCountValid);
+    if (!isCountValid)
+      return NS_ERROR_FAILURE;
+
+    *aAnchorCount = anchorCount;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetURI(int32_t aIndex, nsIURI** aURI)
 {
   NS_ENSURE_ARG_POINTER(aURI);
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount()))
+  if (aIndex < 0)
     return NS_ERROR_INVALID_ARG;
 
-  RefPtr<nsIURI>(Intl()->AnchorURIAt(aIndex)).forget(aURI);
+  if (Intl().IsAccessible()) {
+    if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount()))
+      return NS_ERROR_INVALID_ARG;
+
+    RefPtr<nsIURI>(Intl().AsAccessible()->AnchorURIAt(aIndex)).forget(aURI);
+  } else {
+    nsCString spec;
+    bool isURIValid = false;
+    Intl().AsProxy()->AnchorURIAt(aIndex, spec, &isURIValid);
+    if (!isURIValid)
+      return NS_ERROR_FAILURE;
+
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    uri.forget(aURI);
+  }
+
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetAnchor(int32_t aIndex, nsIAccessible** aAccessible)
 {
   NS_ENSURE_ARG_POINTER(aAccessible);
   *aAccessible = nullptr;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
-
-  if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount()))
+  
+  if (aIndex < 0)
     return NS_ERROR_INVALID_ARG;
 
-  NS_IF_ADDREF(*aAccessible = ToXPC(Intl()->AnchorAt(aIndex)));
+  if (Intl().IsAccessible()) {
+    if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount()))
+      return NS_ERROR_INVALID_ARG;
+
+    NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsAccessible()->AnchorAt(aIndex)));
+  } else {
+    NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsProxy()->AnchorAt(aIndex)));
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetValid(bool* aValid)
 {
   NS_ENSURE_ARG_POINTER(aValid);
   *aValid = false;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aValid = Intl()->IsLinkValid();
+  if (Intl().IsAccessible()) {
+    *aValid = Intl().AsAccessible()->IsLinkValid();
+  } else {
+    *aValid = Intl().AsProxy()->IsLinkValid();
+  }
+
   return NS_OK;
 }
--- a/accessible/xpcom/xpcAccessibleHyperLink.h
+++ b/accessible/xpcom/xpcAccessibleHyperLink.h
@@ -34,15 +34,15 @@ public:
 protected:
   xpcAccessibleHyperLink() { }
   virtual ~xpcAccessibleHyperLink() {}
 
 private:
   xpcAccessibleHyperLink(const xpcAccessibleHyperLink&) = delete;
   xpcAccessibleHyperLink& operator =(const xpcAccessibleHyperLink&) = delete;
 
-  Accessible* Intl();
+  AccessibleOrProxy Intl();
 };
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -8,17 +8,18 @@
 # 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/.
 
 # Makefile.in uses a misc target through test_addons_TARGET.
 HAS_MISC_RULE = True
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
-JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
+JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini',
+                              'source/test/leak/jetpack-package.ini']
 JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
 
 addons = [
     'addon-manager',
     'author-email',
     'child_process',
     'chrome',
     'content-permissions',
--- a/addon-sdk/source/lib/sdk/event/utils.js
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 var { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
+const { Cu } = require("chrome");
 
 // This module provides set of high order function for working with event
 // streams (streams in a NodeJS style that dispatch data, end and error
 // events).
 
 // Function takes a `target` object and returns set of implicit references
 // (non property references) it keeps. This basically allows defining
 // references between objects without storing the explicitly. See transform for
@@ -21,17 +22,17 @@ var refs = (function() {
   let refSets = new WeakMap();
   return function refs(target) {
     if (!refSets.has(target)) refSets.set(target, new Set());
     return refSets.get(target);
   };
 })();
 
 function transform(input, f) {
-  let output = {};
+  let output = new Output();
 
   // Since event listeners don't prevent `input` to be GC-ed we wanna presrve
   // it until `output` can be GC-ed. There for we add implicit reference which
   // is removed once `input` ends.
   refs(output).add(input);
 
   const next = data => receive(output, data);
   once(output, "start", () => start(input));
@@ -59,17 +60,17 @@ exports.filter = filter;
 // mapped via given `f` function.
 const map = (input, f) => transform(input, (data, next) => next(f(data)));
 exports.map = map;
 
 // High order function that takes `input` stream of streams and merges them
 // into single event stream. Like flatten but time based rather than order
 // based.
 function merge(inputs) {
-  let output = {};
+  let output = new Output();
   let open = 1;
   let state = [];
   output.state = state;
   refs(output).add(inputs);
 
   function end(input) {
     open = open - 1;
     refs(output).delete(input);
@@ -102,23 +103,28 @@ exports.merge = merge;
 const expand = (inputs, f) => merge(map(inputs, f));
 exports.expand = expand;
 
 const pipe = (from, to) => on(from, "*", emit.bind(emit, to));
 exports.pipe = pipe;
 
 
 // Shim signal APIs so other modules can be used as is.
-
 const receive = (input, message) => {
   if (input[receive])
     input[receive](input, message);
   else
     emit(input, "data", message);
 
+  // Ideally our input will extend Input and already provide a weak value
+  // getter.  If not, opportunistically shim the weak value getter on
+  // other types passed as the input.
+  if (!("value" in input)) {
+    Object.defineProperty(input, "value", WeakValueGetterSetter);
+  }
   input.value = message;
 };
 receive.toString = () => "@@receive";
 exports.receive = receive;
 exports.send = receive;
 
 const end = input => {
   if (input[end])
@@ -146,17 +152,17 @@ const start = input => {
 };
 start.toString = () => "@@start";
 exports.start = start;
 
 const lift = (step, ...inputs) => {
   let args = null;
   let opened = inputs.length;
   let started = false;
-  const output = {};
+  const output = new Output();
   const init = () => {
     args = [...inputs.map(input => input.value)];
     output.value = step(...args);
   };
 
   inputs.forEach((input, index) => {
     on(input, "data", data => {
       args[index] = data;
@@ -177,17 +183,18 @@ const lift = (step, ...inputs) => {
   init();
 
   return output;
 };
 exports.lift = lift;
 
 const merges = inputs => {
   let opened = inputs.length;
-  let output = { value: inputs[0].value };
+  let output = new Output();
+  output.value = inputs[0].value;
   inputs.forEach((input, index) => {
     on(input, "data", data => receive(output, data));
     on(input, "end", () => {
       opened = opened - 1;
       if (opened <= 0)
         end(output);
     });
   });
@@ -220,22 +227,56 @@ Input.start = input => emit(input, "star
 Input.prototype.start = Input.start;
 
 Input.end = input => {
   emit(input, "end", input);
   stop(input);
 };
 Input.prototype[end] = Input.end;
 
+// The event channel system caches the last event seen as input.value.
+// Unfortunately, if the last event is a DOM object this is a great way
+// leak windows.  Mitigate this by storing input.value using a weak
+// reference.  This allows the system to work for normal event processing
+// while also allowing the objects to be reclaimed.  It means, however,
+// input.value cannot be accessed long after the event was dispatched.
+const WeakValueGetterSetter = {
+  get: function() {
+    return this._weakValue ? this._weakValue.get() : this._simpleValue
+  },
+  set: function(v) {
+    if (v && typeof v === "object") {
+      this._weakValue = Cu.getWeakReference(v)
+      this._simpleValue = undefined;
+      return;
+    }
+    this._simpleValue = v;
+    this._weakValue = undefined;
+  },
+}
+Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter);
+
 exports.Input = Input;
 
+// Define an Output type with a weak value getter for the transformation
+// functions that produce new channels.
+function Output() { }
+Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter);
+exports.Output = Output;
+
 const $source = "@@source";
 const $outputs = "@@outputs";
 exports.outputs = $outputs;
 
+// NOTE: Passing DOM objects through a Reactor can cause them to leak
+// when they get cached in this.value.  We cannot use a weak reference
+// in this case because the Reactor design expects to always have both the
+// past and present value.  If we allow past values to be collected the
+// system breaks.
+
 function Reactor(options={}) {
   const {onStep, onStart, onEnd} = options;
   if (onStep)
     this.onStep = onStep;
   if (onStart)
     this.onStart = onStart;
   if (onEnd)
     this.onEnd = onEnd;
--- a/addon-sdk/source/lib/sdk/window/events.js
+++ b/addon-sdk/source/lib/sdk/window/events.js
@@ -2,32 +2,50 @@
  * 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 { Ci } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const { observe } = require("../event/chrome");
 const { open } = require("../event/dom");
 const { windows } = require("../window/utils");
 const { filter, merge, map, expand } = require("../event/utils");
 
+function documentMatches(weakWindow, event) {
+  let window = weakWindow.get();
+  return window && event.target === window.document;
+}
+
+function makeStrictDocumentFilter(window) {
+  // Note: Do not define a closure within this function.  Otherwise
+  //       you may leak the window argument.
+  let weak = Cu.getWeakReference(window);
+  return documentMatches.bind(null, weak);
+}
+
+function toEventWithDefaultViewTarget({type, target}) {
+  return { type: type, target: target.defaultView }
+}
+
 // Function registers single shot event listeners for relevant window events
 // that forward events to exported event stream.
 function eventsFor(window) {
+  // NOTE: Do no use pass a closure from this function into a stream
+  //       transform function.  You will capture the window in the
+  //       closure and leak the window until the event stream is
+  //       completely closed.
   let interactive = open(window, "DOMContentLoaded", { capture: true });
   let complete = open(window, "load", { capture: true });
   let states = merge([interactive, complete]);
-  let changes = filter(states, ({target}) => target === window.document);
-  return map(changes, function({type, target}) {
-    return { type: type, target: target.defaultView }
-  });
+  let changes = filter(states, makeStrictDocumentFilter(window));
+  return map(changes, toEventWithDefaultViewTarget);
 }
 
 // In addition to observing windows that are open we also observe windows
 // that are already already opened in case they're in process of loading.
 var opened = windows(null, { includePrivate: true });
 var currentEvents = merge(opened.map(eventsFor));
 
 // Register system event listeners for top level window open / close.
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/jetpack-package.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  leak-utils.js
+
+[test-leak-window-events.js]
+[test-leak-event-dom-closed-window.js]
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/leak-utils.js
@@ -0,0 +1,80 @@
+/* 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 { Cu, Ci } = require("chrome");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { SelfSupportBackend } = Cu.import("resource:///modules/SelfSupportBackend.jsm", {});
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
+
+// Adapted from the SpecialPowers.exactGC() code.  We don't have a
+// window to operate on so we cannot use the exact same logic.  We
+// use 6 GC iterations here as that is what is needed to clean up
+// the windows we have tested with.
+function gc() {
+  return new Promise(resolve => {
+    Cu.forceGC();
+    Cu.forceCC();
+    let count = 0;
+    function genGCCallback() {
+      Cu.forceCC();
+      return function() {
+        if (++count < 5) {
+          Cu.schedulePreciseGC(genGCCallback());
+        } else {
+          resolve();
+        }
+      }
+    }
+
+    Cu.schedulePreciseGC(genGCCallback());
+  });
+}
+
+// Execute the given test function and verify that we did not leak windows
+// in the process.  The test function must return a promise or be a generator.
+// If the promise is resolved, or generator completes, with an sdk loader
+// object then it will be unloaded after the memory measurements.
+exports.asyncWindowLeakTest = function*(assert, asyncTestFunc) {
+
+  // SelfSupportBackend periodically tries to open windows.  This can
+  // mess up our window leak detection below, so turn it off.
+  SelfSupportBackend.uninit();
+
+  // Wait for the browser to finish loading.
+  yield Startup.onceInitialized;
+
+  // Track windows that are opened in an array of weak references.
+  let weakWindows = [];
+  function windowObserver(subject, topic) {
+    let supportsWeak = subject.QueryInterface(Ci.nsISupportsWeakReference);
+    if (supportsWeak) {
+      weakWindows.push(Cu.getWeakReference(supportsWeak));
+    }
+  }
+  Services.obs.addObserver(windowObserver, "domwindowopened", false);
+
+  // Execute the body of the test.
+  let testLoader = yield asyncTestFunc(assert);
+
+  // Stop tracking new windows and attempt to GC any resources allocated
+  // by the test body.
+  Services.obs.removeObserver(windowObserver, "domwindowopened", false);
+  yield gc();
+
+  // Check to see if any of the windows we saw survived the GC.  We consider
+  // these leaks.
+  assert.ok(weakWindows.length > 0, "should see at least one new window");
+  for (let i = 0; i < weakWindows.length; ++i) {
+    assert.equal(weakWindows[i].get(), null, "window " + i + " should be GC'd");
+  }
+
+  // Finally, unload the test body's loader if it provided one.  We do this
+  // after our leak detection to avoid free'ing things on unload.  Users
+  // don't tend to unload their addons very often, so we want to find leaks
+  // that happen while addons are in use.
+  if (testLoader) {
+    testLoader.unload();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js
@@ -0,0 +1,29 @@
+/* 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 { asyncWindowLeakTest } = require("./leak-utils");
+const { Loader } = require('sdk/test/loader');
+const openWindow = require("sdk/window/utils").open;
+
+exports["test sdk/event/dom does not leak when attached to closed window"] = function*(assert) {
+  yield asyncWindowLeakTest(assert, _ => {
+    return new Promise(resolve => {
+      let loader = Loader(module);
+      let { open } = loader.require('sdk/event/dom');
+      let w = openWindow();
+      w.addEventListener("DOMWindowClose", function windowClosed(evt) {
+        w.removeEventListener("DOMWindowClose", windowClosed);
+        // The sdk/event/dom module tries to clean itself up when DOMWindowClose
+        // is fired.  Verify that it doesn't leak if its attached to an
+        // already closed window either. (See bug 1268898.)
+        open(w.document, "TestEvent1");
+        resolve(loader);
+      });
+      w.close();
+    });
+  });
+}
+
+require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/test-leak-window-events.js
@@ -0,0 +1,47 @@
+/* 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";
+
+// Opening new windows in Fennec causes issues
+module.metadata = {
+  engines: {
+    'Firefox': '*'
+  }
+};
+
+const { asyncWindowLeakTest } = require("./leak-utils.js");
+const { Loader } = require("sdk/test/loader");
+const { open } = require("sdk/window/utils");
+
+exports["test window/events for leaks"] = function*(assert) {
+  yield asyncWindowLeakTest(assert, _ => {
+    return new Promise((resolve, reject) => {
+      let loader = Loader(module);
+      let { events } = loader.require("sdk/window/events");
+      let { on, off } = loader.require("sdk/event/core");
+
+      on(events, "data", function handler(e) {
+        try {
+          if (e.type === "load") {
+            e.target.close();
+          }
+          else if (e.type === "close") {
+            off(events, "data", handler);
+
+            // Let asyncWindowLeakTest call loader.unload() after the
+            // leak check.
+            resolve(loader);
+          }
+        } catch (e) {
+          reject(e);
+        }
+      });
+
+      // Open a window.  This will trigger our data events.
+      open();
+    });
+  });
+};
+
+require("sdk/test").run(exports);
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1101,16 +1101,22 @@ pref("services.sync.prefs.sync.spellchec
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 
 // A preference that controls whether we should show the icon for a remote tab.
 // This pref has no UI but exists because some people may be concerned that
 // fetching these icons to show remote tabs may leak information about that
 // user's tabs and bookmarks. Note this pref is also synced.
 pref("services.sync.syncedTabs.showRemoteIcons", true);
 
+#ifdef NIGHTLY_BUILD
+pref("services.sync.sendTabToDevice.enabled", true);
+#else
+pref("services.sync.sendTabToDevice.enabled", false);
+#endif
+
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
 sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
 #else
 sticky_pref("lightweightThemes.selectedThemeID", "");
 #endif
 
 // Whether the character encoding menu is under the main Firefox button. This
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -278,16 +278,24 @@
       <menuitem id="context-sharepage"
                 label="&sharePageCmd.label;"
                 accesskey="&sharePageCmd.accesskey;"
                 oncommand="SocialShare.sharePage();"/>
       <menuitem id="context-savepage"
                 label="&savePageCmd.label;"
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
+      <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
+      <menu id="context-sendpagetodevice"
+                label="&sendPageToDevice.label;"
+                accesskey="&sendPageToDevice.accesskey;"
+                hidden="true">
+        <menupopup id="context-sendpagetodevice-popup"
+                   onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
+      </menu>
       <menu id="context-markpageMenu" label="&social.markpageMenu.label;"
             accesskey="&social.markpageMenu.accesskey;">
         <menupopup/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
@@ -321,16 +329,24 @@
                 command="cmd_selectAll"/>
       <menuseparator id="context-sep-selectall"/>
       <menuitem id="context-keywordfield"
                 label="&keywordfield.label;"
                 accesskey="&keywordfield.accesskey;"
                 oncommand="AddKeywordForSearchField();"/>
       <menuitem id="context-searchselect"
                 oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
+      <menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
+      <menu id="context-sendlinktodevice"
+                label="&sendLinkToDevice.label;"
+                accesskey="&sendLinkToDevice.accesskey;"
+                hidden="true">
+        <menupopup id="context-sendlinktodevice-popup"
+                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+      </menu>
       <menuitem id="context-shareselect"
                 label="&shareSelect.label;"
                 accesskey="&shareSelect.accesskey;"
                 oncommand="gContextMenu.shareSelect();"/>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
           <menuitem id="context-showonlythisframe"
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -73,16 +73,25 @@ var gFxAccounts = {
       return false;
     }
     // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
     // All other login failures are assumed to be transient and should go
     // away by themselves, so aren't reflected here.
     return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
+  get sendTabToDeviceEnabled() {
+    return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+  },
+
+  get remoteClients() {
+    return Weave.Service.clientsEngine.remoteClients
+           .sort((a, b) => a.name.localeCompare(b.name));
+  },
+
   init: function () {
     // Bail out if we're already initialized and for pop-up windows.
     if (this._initialized || !window.toolbar.visible) {
       return;
     }
 
     for (let topic of this.topics) {
       Services.obs.addObserver(this, topic, false);
@@ -356,16 +365,91 @@ var gFxAccounts = {
     switchToTabHavingURI(url, true, {
       replaceQueryString: true
     });
   },
 
   openSignInAgainPage: function (entryPoint) {
     this.openAccountsPage("reauth", { entrypoint: entryPoint });
   },
+
+  sendTabToDevice: function (url, clientId, title) {
+    Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+  },
+
+  populateSendTabToDevicesMenu: function (devicesPopup, url, title) {
+    // remove existing menu items
+    while (devicesPopup.hasChildNodes()) {
+      devicesPopup.removeChild(devicesPopup.firstChild);
+    }
+
+    const fragment = document.createDocumentFragment();
+
+    const onTargetDeviceCommand = (event) => {
+      const clientId = event.target.getAttribute("clientId");
+      const clients = clientId
+                      ? [clientId]
+                      : this.remoteClients.map(client => client.id);
+
+      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+    }
+
+    function addTargetDevice(clientId, name) {
+      const targetDevice = document.createElement("menuitem");
+      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+      targetDevice.setAttribute("class", "sendtab-target");
+      targetDevice.setAttribute("clientId", clientId);
+      targetDevice.setAttribute("label", name);
+      fragment.appendChild(targetDevice);
+    }
+
+    const clients = this.remoteClients;
+    for (let client of clients) {
+      addTargetDevice(client.id, client.name);
+    }
+
+    // "All devices" menu item
+    const separator = document.createElement("menuseparator");
+    fragment.appendChild(separator);
+    const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
+    addTargetDevice("", allDevicesLabel);
+
+    devicesPopup.appendChild(fragment);
+  },
+
+  updateTabContextMenu: function (aPopupMenu) {
+    if (!this.sendTabToDeviceEnabled) {
+      return;
+    }
+
+    const remoteClientPresent = this.remoteClients.length > 0;
+    ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+    .forEach(id => { document.getElementById(id).hidden = !remoteClientPresent });
+  },
+
+  initPageContextMenu: function (contextMenu) {
+    if (!this.sendTabToDeviceEnabled) {
+      return;
+    }
+
+    const remoteClientPresent = this.remoteClients.length > 0;
+    // showSendLink and showSendPage are mutually exclusive
+    const showSendLink = remoteClientPresent
+                         && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+    const showSendPage = !showSendLink && remoteClientPresent
+                         && !(contextMenu.isContentSelected ||
+                              contextMenu.onImage || contextMenu.onCanvas ||
+                              contextMenu.onVideo || contextMenu.onAudio ||
+                              contextMenu.onLink || contextMenu.onTextInput);
+
+    ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendPage));
+    ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendLink));
+  }
 };
 
 XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
   return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
 XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
   "resource://services-sync/FxaMigrator.jsm");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -322,16 +322,46 @@ toolbarpaletteitem > toolbaritem[sdkstyl
 toolbarpaletteitem:-moz-any([place="palette"], [place="panel"]) > toolbaritem[sdkstylewidget="true"] > toolbarbutton {
   display: -moz-box;
 }
 
 toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] > .toolbarbutton-text {
   display: -moz-box;
 }
 
+@media not all and (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image);
+  }
+}
+
+@media (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image-2x);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image-2x);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image-2x);
+  }
+}
+
 toolbarpaletteitem[removable="false"] {
   opacity: 0.5;
   cursor: default;
 }
 
 %ifndef XP_MACOSX
 toolbarpaletteitem[place="palette"],
 toolbarpaletteitem[place="panel"],
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7616,16 +7616,18 @@ var TabContextMenu = {
       toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
     }
 
     this.contextTab.toggleMuteMenuItem = toggleMute;
     this._updateToggleMuteMenuItem(this.contextTab);
 
     this.contextTab.addEventListener("TabAttrModified", this, false);
     aPopupMenu.addEventListener("popuphiding", this, false);
+
+    gFxAccounts.updateTabContextMenu(aPopupMenu);
   },
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphiding":
         gBrowser.removeEventListener("TabAttrModified", this);
         aEvent.target.removeEventListener("popuphiding", this);
         break;
       case "TabAttrModified":
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -99,16 +99,22 @@
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
 #ifdef E10S_TESTING_ONLY
       <menuitem id="context_openNonRemoteWindow" label="Open in new non-e10s window"
                 tbattr="tabbrowser-remote"
                 hidden="true"
                 oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
 #endif
+      <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
+      <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
+            accesskey="&sendTabToDevice.accesskey;" hidden="true">
+        <menupopup id="context_sendTabToDevicePopupMenu"
+                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+      </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
       <menuitem id="context_bookmarkAllTabs"
                 label="&bookmarkAllTabs.label;"
                 accesskey="&bookmarkAllTabs.accesskey;"
                 command="Browser:BookmarkAllTabs"/>
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -112,16 +112,17 @@ nsContextMenu.prototype = {
     this.initMiscItems();
     this.initSpellingItems();
     this.initSaveItems();
     this.initClipboardItems();
     this.initMediaPlayerItems();
     this.initLeaveDOMFullScreenItems();
     this.initClickToPlayItems();
     this.initPasswordManagerItems();
+    this.initSyncItems();
   },
 
   initPageMenuSeparator: function CM_initPageMenuSeparator() {
     this.showItem("page-menu-separator", this.hasPageMenu);
   },
 
   initOpenItems: function CM_initOpenItems() {
     var isMailtoInternal = false;
@@ -571,16 +572,20 @@ nsContextMenu.prototype = {
     if (!fragment) {
       return;
     }
     let popup = document.getElementById("fill-login-popup");
     let insertBeforeElement = document.getElementById("fill-login-no-logins");
     popup.insertBefore(fragment, insertBeforeElement);
   },
 
+  initSyncItems: function() {
+    gFxAccounts.initPageContextMenu(this);
+  },
+
   openPasswordManager: function() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode: function() {
     let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
     let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
     let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1915,16 +1915,19 @@
             // set the "nodefaultsrc" attribute that prevents a frameLoader
             // from being created as soon as the linked <browser> is inserted
             // into the DOM. We thus have to register the new outerWindowID
             // for non-remote browsers after we have called browser.loadURI().
             if (!remote) {
               this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
             }
 
+            var evt = new CustomEvent("TabBrowserCreated", { bubbles: true, detail: {} });
+            aTab.dispatchEvent(evt);
+
             return { usingPreloadedContent: usingPreloadedContent };
           ]]>
         </body>
       </method>
 
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -847,16 +847,90 @@ add_task(function* test_input_spell_fals
      "---",                 null,
      "context-selectall",   true,
      "---",                 null,
      "spell-add-dictionaries-main",  true,
     ]
   );
 });
 
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+add_task(function* test_plaintext_sendpagetodevice() {
+  if (!gFxAccounts.sendTabToDeviceEnabled) {
+    return;
+  }
+  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+  let plainTextItems = ["context-navigation",   null,
+                        ["context-back",         false,
+                         "context-forward",      false,
+                         "context-reload",       true,
+                         "context-bookmarkpage", true], null,
+                    "---",                  null,
+                    "context-savepage",     true,
+                    ...(hasPocket ? ["context-pocket", true] : []),
+                    "---",                  null,
+                    "context-sendpagetodevice", true,
+                      ["*Foo", true,
+                       "*Bar", true,
+                       "---", null,
+                       "*All Devices", true], null,
+                    "---",                  null,
+                    "context-viewbgimage",  false,
+                    "context-selectall",    true,
+                    "---",                  null,
+                    "context-viewsource",   true,
+                    "context-viewinfo",     true
+                   ];
+  yield test_contextmenu("#test-text", plainTextItems, {
+      onContextMenuShown() {
+        yield openMenuItemSubmenu("context-sendpagetodevice");
+      }
+    });
+
+  restoreRemoteClients(oldGetter);
+});
+
+add_task(function* test_link_sendlinktodevice() {
+  if (!gFxAccounts.sendTabToDeviceEnabled) {
+    return;
+  }
+  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+  yield test_contextmenu("#test-link",
+    ["context-openlinkintab", true,
+     ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+     // We need a blank entry here because the containers submenu is
+     // dynamically generated with no ids.
+     ...(hasContainers ? ["", null] : []),
+     "context-openlink",      true,
+     "context-openlinkprivate", true,
+     "---",                   null,
+     "context-bookmarklink",  true,
+     "context-savelink",      true,
+     ...(hasPocket ? ["context-savelinktopocket", true] : []),
+     "context-copylink",      true,
+     "context-searchselect",  true,
+     "---",                  null,
+     "context-sendlinktodevice", true,
+      ["*Foo", true,
+       "*Bar", true,
+       "---", null,
+       "*All Devices", true], null,
+    ],
+    {
+      onContextMenuShown() {
+        yield openMenuItemSubmenu("context-sendlinktodevice");
+      }
+    });
+
+  restoreRemoteClients(oldGetter);
+});
+
 add_task(function* test_cleanup() {
   gBrowser.removeCurrentTab();
 });
 
 /**
  * Selects the text of the element that matches the provided `selector`
  *
  * @param {String} selector
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -1,24 +1,41 @@
 /* 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 remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
 add_task(function* test() {
   // There should be one tab when we start the test
   let [origTab] = gBrowser.visibleTabs;
   is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
   let testTab = gBrowser.addTab();
   is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
 
   // Check the context menu with two tabs
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
   is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
 
+
+  if (gFxAccounts.sendTabToDeviceEnabled) {
+    // Check the send tab to device menu item
+    const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+    yield updateTabContextMenu(origTab, function* () {
+      yield openMenuItemSubmenu("context_sendTabToDevice");
+    });
+    is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
+    let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+    is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
+    is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
+    is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
+    restoreRemoteClients(oldGetter);
+  }
+
   // Hide the original tab.
   gBrowser.selectedTab = testTab;
   gBrowser.showOnlyTheseTabs([testTab]);
   is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
   
   // Check the context menu with one tab.
   updateTabContextMenu(testTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists");
--- a/browser/base/content/test/general/contextmenu_common.js
+++ b/browser/base/content/test/general/contextmenu_common.js
@@ -36,69 +36,70 @@ function getVisibleMenuItems(aMenu, aDat
         var item = aMenu.childNodes[i];
         if (item.hidden)
             continue;
 
         var key = item.accessKey;
         if (key)
             key = key.toLowerCase();
 
-        var isGenerated = item.hasAttribute("generateditemid");
+        var isPageMenuItem = item.hasAttribute("generateditemid");
 
         if (item.nodeName == "menuitem") {
-            var isSpellSuggestion = item.className == "spell-suggestion";
-            if (isSpellSuggestion) {
-              is(item.id, "", "child menuitem #" + i + " is a spelling suggestion");
-            } else if (isGenerated) {
-              is(item.id, "", "child menuitem #" + i + " is a generated item");
+            var isGenerated = item.className == "spell-suggestion"
+                              || item.className == "sendtab-target";
+            if (isGenerated) {
+              is(item.id, "", "child menuitem #" + i + " is generated");
+            } else if (isPageMenuItem) {
+              is(item.id, "", "child menuitem #" + i + " is a generated page menu item");
             } else {
               ok(item.id, "child menuitem #" + i + " has an ID");
             }
             var label = item.getAttribute("label");
             ok(label.length, "menuitem " + item.id + " has a label");
-            if (isSpellSuggestion) {
-              is(key, "", "Spell suggestions shouldn't have an access key");
+            if (isGenerated) {
+              is(key, "", "Generated items shouldn't have an access key");
               items.push("*" + label);
-            } else if (isGenerated) {
+            } else if (isPageMenuItem) {
               items.push("+" + label);
             } else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
                        item.id != "spell-no-suggestions" &&
                        item.id != "spell-add-dictionaries-main" &&
                        item.id != "context-savelinktopocket" &&
                        item.id != "fill-login-saved-passwords" &&
                        item.id != "fill-login-no-logins") {
               ok(key, "menuitem " + item.id + " has an access key");
               if (accessKeys[key])
                   ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);
               else
                   accessKeys[key] = item.id;
             }
-            if (!isSpellSuggestion && !isGenerated) {
+            if (!isGenerated && !isPageMenuItem) {
               items.push(item.id);
             }
-            if (isGenerated) {
+            if (isPageMenuItem) {
               var p = {};
               p.type = item.getAttribute("type");
               p.icon = item.getAttribute("image");
               p.checked = item.hasAttribute("checked");
               p.disabled = item.hasAttribute("disabled");
               items.push(p);
             } else {
               items.push(!item.disabled);
             }
         } else if (item.nodeName == "menuseparator") {
             ok(true, "--- seperator id is " + item.id);
             items.push("---");
             items.push(null);
         } else if (item.nodeName == "menu") {
-            if (isGenerated) {
+            if (isPageMenuItem) {
                 item.id = "generated-submenu-" + aData.generatedSubmenuId++;
             }
             ok(item.id, "child menu #" + i + " has an ID");
-            if (!isGenerated) {
+            if (!isPageMenuItem) {
                 ok(key, "menu has an access key");
                 if (accessKeys[key])
                     ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]);
                 else
                     accessKeys[key] = item.id;
             }
             items.push(item.id);
             items.push(!item.disabled);
@@ -237,16 +238,17 @@ let lastElementSelector = null;
  *                 the element, optional
  *        offsetY: vertical mouse offset from the top-left corner of the
  *                 element, optional
  *        centered: if true, mouse position is centered in element, defaults
  *                  to true if offsetX and offsetY are not provided
  *        waitForSpellCheck: wait until spellcheck is initialized before
  *                           starting test
  *        preCheckContextMenuFn: callback to run before opening menu
+ *        onContextMenuShown: callback to run when the context menu is shown
  *        postCheckContextMenuFn: callback to run after opening menu
  * @return {Promise} resolved after the test finishes
  */
 function* test_contextmenu(selector, menuItems, options={}) {
   contextMenu = document.getElementById("contentAreaContextMenu");
   is(contextMenu.state, "closed", "checking if popup is closed");
 
   // Default to centered if no positioning is defined.
@@ -290,16 +292,21 @@ function* test_contextmenu(selector, men
       button: 2,
       shiftkey: options.shiftkey,
       centered: options.centered
     },
     gBrowser.selectedBrowser);
   yield awaitPopupShown;
   info("Popup Shown");
 
+  if (options.onContextMenuShown) {
+    yield options.onContextMenuShown();
+    info("Completed onContextMenuShown");
+  }
+
   if (menuItems) {
     if (Services.prefs.getBoolPref("devtools.inspector.enabled")) {
       let inspectItems = ["---", null,
                           "context-inspect", true];
       menuItems = menuItems.concat(inspectItems);
     }
 
     checkContextMenu(menuItems);
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -56,25 +56,33 @@ function whenDelayedStartupFinished(aWin
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
 
-function updateTabContextMenu(tab) {
+function updateTabContextMenu(tab, onOpened) {
   let menu = document.getElementById("tabContextMenu");
   if (!tab)
     tab = gBrowser.selectedTab;
   var evt = new Event("");
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
-  menu.hidePopup();
+  const onFinished = () => menu.hidePopup();
+  if (onOpened) {
+    return Task.spawn(function*() {
+      yield onOpened();
+      onFinished();
+    });
+  } else {
+    onFinished();
+  }
 }
 
 function openToolbarCustomizationUI(aCallback, aBrowserWin) {
   if (!aBrowserWin)
     aBrowserWin = window;
 
   aBrowserWin.gCustomizeMode.enter();
 
@@ -1154,8 +1162,30 @@ function getCertExceptionDialog(aLocatio
 
       if (childDoc.location.href == aLocation) {
         return childDoc;
       }
     }
   }
 }
 
+function setupRemoteClientsFixture(fixture) {
+  let oldRemoteClientsGetter =
+    Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+
+  Object.defineProperty(gFxAccounts, "remoteClients", {
+    get: function() { return fixture; }
+  });
+  return oldRemoteClientsGetter;
+}
+
+function restoreRemoteClients(getter) {
+  Object.defineProperty(gFxAccounts, "remoteClients", {
+    get: getter
+  });
+}
+
+function* openMenuItemSubmenu(id) {
+  let menuPopup = document.getElementById(id).menupopup;
+  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+  menuPopup.showPopup();
+  yield menuPopupPromise;
+}
--- a/browser/base/content/web-panels.xul
+++ b/browser/base/content/web-panels.xul
@@ -19,16 +19,17 @@
 <page id="webpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="load()" onunload="unload()">
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
+  <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
   <script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
   <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
   <script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
 
   <stringbundleset id="stringbundleset"> 
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
   </stringbundleset>
 
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -74,16 +74,17 @@ BrowserAction.prototype = {
         let view = document.getElementById(this.viewId);
         if (view) {
           view.remove();
         }
       },
 
       onCreated: node => {
         node.classList.add("badged-button");
+        node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
 
         this.updateButton(node, this.defaults);
       },
 
       onViewShowing: event => {
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
@@ -145,32 +146,44 @@ BrowserAction.prototype = {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
       badgeNode.style.backgroundColor = color || "";
     }
 
     const LEGACY_CLASS = "toolbarbutton-legacy-addon";
     node.classList.remove(LEGACY_CLASS);
 
-
-    let win = node.ownerDocument.defaultView;
-    let {icon, size} = IconDetails.getURL(tabData.icon, win, this.extension);
+    let baseSize = 16;
+    let {icon, size} = IconDetails.getPreferredIcon(tabData.icon, this.extension, baseSize);
 
     // If the best available icon size is not divisible by 16, check if we have
     // an 18px icon to fall back to, and trim off the padding instead.
     if (size % 16 && !icon.endsWith(".svg")) {
-      let result = IconDetails.getURL(tabData.icon, win, this.extension, 18);
+      let result = IconDetails.getPreferredIcon(tabData.icon, this.extension, 18);
 
       if (result.size % 18 == 0) {
+        baseSize = 18;
         icon = result.icon;
         node.classList.add(LEGACY_CLASS);
       }
     }
 
-    node.setAttribute("image", icon);
+    // These URLs should already be properly escaped, but make doubly sure CSS
+    // string escape characters are escaped here, since they could lead to a
+    // sandbox break.
+    let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+    let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+    node.setAttribute("style", `
+      --webextension-menupanel-image: url("${getIcon(32)}");
+      --webextension-menupanel-image-2x: url("${getIcon(64)}");
+      --webextension-toolbar-image: url("${escape(icon)}");
+      --webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
+    `);
   },
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -49,17 +49,22 @@ var gMenuBuilder = {
       rootElement.setAttribute("ext-type", "top-level-menu");
       rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
 
       // Display the extension icon on the root element.
       if (root.extension.manifest.icons) {
         let parentWindow = contextData.menu.ownerDocument.defaultView;
         let extension = root.extension;
 
-        let {icon} = IconDetails.getURL(extension.manifest.icons, parentWindow, extension, 16 /* size */);
+        let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
+                                                  16 * parentWindow.devicePixelRatio);
+
+        // The extension icons in the manifest are not pre-resolved, since
+        // they're sometimes used by the add-on manager when the extension is
+        // not enabled, and its URLs are not resolvable.
         let resolvedURL = root.extension.baseURI.resolve(icon);
 
         if (rootElement.localName == "menu") {
           rootElement.setAttribute("class", "menu-iconic");
         } else if (rootElement.localName == "menuitem") {
           rootElement.setAttribute("class", "menuitem-iconic");
         }
         rootElement.setAttribute("image", resolvedURL);
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -87,18 +87,29 @@ PageAction.prototype = {
 
     if (tabData.show) {
       // Update the title and icon only if the button is visible.
 
       let title = tabData.title || this.extension.name;
       button.setAttribute("tooltiptext", title);
       button.setAttribute("aria-label", title);
 
-      let {icon} = IconDetails.getURL(tabData.icon, window, this.extension);
-      button.setAttribute("src", icon);
+      // These URLs should already be properly escaped, but make doubly sure CSS
+      // string escape characters are escaped here, since they could lead to a
+      // sandbox break.
+      let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+      let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+      button.setAttribute("style", `
+        --webextension-urlbar-image: url("${getIcon(16)}");
+        --webextension-urlbar-image-2x: url("${getIcon(32)}");
+      `);
+
+      button.classList.add("webextension-page-action");
     }
 
     button.hidden = !tabData.show;
   },
 
   // Create an |image| node and add it to the |urlbar-icons|
   // container in the given window.
   addButton(window) {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -92,17 +92,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let button = document.getElementById(browserActionId);
 
     ok(button, "button exists");
 
     let title = details.title || options.manifest.name;
 
-    is(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
     is(button.getAttribute("label"), title, "image label is correct");
     is(button.getAttribute("badge"), details.badge, "badge text is correct");
     is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
 
     if (details.badge && details.badgeBackgroundColor) {
       let badge = button.ownerDocument.getAnonymousElementByAttribute(
         button, "class", "toolbarbutton-badge");
@@ -158,16 +158,21 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "default-2.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs, expectDefaults) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
@@ -317,16 +322,20 @@ add_task(function* testDefaultTitle() {
 
       "browser_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs, expectDefaults) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "badge": "",
          "badgeBackgroundColor": null,
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -47,16 +47,22 @@ add_task(function* testDetailsObjects() 
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a.png")}},
       {details: {"path": {"19": "a.png", "38": "a-x2.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a-x2.png")}},
 
+      // Test that CSS strings are escaped properly.
+      {details: {"path": 'a.png#" \\'},
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png#%22%20%5C"),
+          "2": browser.runtime.getURL("data/a.png#%22%20%5C")}},
+
       // Only ImageData objects.
       {details: {"imageData": imageData.red.imageData},
         resolutions: {
           "1": imageData.red.url,
           "2": imageData.red.url}},
       {details: {"imageData": {"19": imageData.red.imageData}},
         resolutions: {
           "1": imageData.red.url,
@@ -131,26 +137,33 @@ add_task(function* testDetailsObjects() 
         "6": "6.png",
         "18": "18.png",
         "36": "36.png",
         "48": "48.png",
         "128": "128.png"}},
         legacy: true,
         resolutions: {
           "1": browser.runtime.getURL("data/18.png"),
-          "2": browser.runtime.getURL("data/36.png")}},
+          "2": browser.runtime.getURL("data/36.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/36.png"),
+          "2": browser.runtime.getURL("data/128.png")}},
       {details: {"path": {
         "16": "16.png",
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
+        "64": "64.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/16.png"),
-          "2": browser.runtime.getURL("data/32.png")}},
+          "2": browser.runtime.getURL("data/32.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/32.png"),
+          "2": browser.runtime.getURL("data/64.png")}},
       {details: {"path": {
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/32.png"),
           "2": browser.runtime.getURL("data/32.png")}},
@@ -162,42 +175,44 @@ add_task(function* testDetailsObjects() 
     let tabId;
 
     browser.test.onMessage.addListener((msg, test) => {
       if (msg != "setIcon") {
         browser.test.fail("expecting 'setIcon' message");
       }
 
       let details = iconDetails[test.index];
-      let expectedURL = details.resolutions[test.resolution];
 
       let detailString = JSON.stringify(details);
-      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`);
+      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URLs ${JSON.stringify(details.resolutions)}`);
 
       browser.browserAction.setIcon(Object.assign({tabId}, details.details));
       browser.pageAction.setIcon(Object.assign({tabId}, details.details));
 
-      browser.test.sendMessage("imageURL", [expectedURL, !!details.legacy]);
+      browser.test.sendMessage("iconSet");
     });
 
     // Generate a list of tests and resolutions to send back to the test
     // context.
     //
     // This process is a bit convoluted, because the outer test context needs
     // to handle checking the button nodes and changing the screen resolution,
     // but it can't pass us icon definitions with ImageData objects. This
     // shouldn't be a problem, since structured clones should handle ImageData
     // objects without issue. Unfortunately, |cloneInto| implements a slightly
     // different algorithm than we use in web APIs, and does not handle them
     // correctly.
     let tests = [];
     for (let [idx, icon] of iconDetails.entries()) {
-      for (let res of Object.keys(icon.resolutions)) {
-        tests.push({index: idx, resolution: Number(res)});
-      }
+      tests.push({
+        index: idx,
+        legacy: !!icon.legacy,
+        menuResolutions: icon.menuResolutions,
+        resolutions: icon.resolutions,
+      });
     }
 
     // Sort by resolution, so we don't needlessly switch back and forth
     // between each test.
     tests.sort(test => test.resolution);
 
     browser.tabs.query({active: true, currentWindow: true}, tabs => {
       tabId = tabs[0].id;
@@ -214,45 +229,96 @@ add_task(function* testDetailsObjects() 
       "background": {
         "page": "data/background.html",
       }
     },
 
     files: {
       "data/background.html": `<script src="background.js"></script>`,
       "data/background.js": background,
+
+      "data/16.svg": imageBuffer,
+      "data/18.svg": imageBuffer,
+
+      "data/16.png": imageBuffer,
+      "data/18.png": imageBuffer,
+      "data/32.png": imageBuffer,
+      "data/36.png": imageBuffer,
+      "data/48.png": imageBuffer,
+      "data/64.png": imageBuffer,
+      "data/128.png": imageBuffer,
+
+      "a.png": imageBuffer,
+      "data/2.png": imageBuffer,
+      "data/100.png": imageBuffer,
+      "data/a.png": imageBuffer,
+      "data/a-x2.png": imageBuffer,
     },
   });
 
   const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
-  registerCleanupFunction(() => {
-    SpecialPowers.clearUserPref(RESOLUTION_PREF);
-  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let browserActionWidget = getBrowserActionWidget(extension);
 
-  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
-  let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+  yield extension.startup();
+  let tests = yield extension.awaitMessage("ready");
 
   for (let test of tests) {
-    SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
-    is(window.devicePixelRatio, test.resolution, "window has the required resolution");
+    extension.sendMessage("setIcon", test);
+    yield extension.awaitMessage("iconSet");
+
+    let browserActionButton = browserActionWidget.forWindow(window).node;
+    let pageActionImage = document.getElementById(pageActionId);
+
+
+    // Test icon sizes in the toolbar/urlbar.
+    for (let resolution of Object.keys(test.resolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
 
-    extension.sendMessage("setIcon", test);
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.resolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct image at ${resolution}x resolution`);
+      is(getListStyleImage(pageActionImage), imageURL, `page action has the correct image at ${resolution}x resolution`);
 
-    let [imageURL, legacy] = yield extension.awaitMessage("imageURL");
+      let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
+      is(isLegacy, test.legacy, "Legacy class should be present?");
+
+      yield SpecialPowers.popPrefEnv();
+    }
 
-    let browserActionButton = document.getElementById(browserActionId);
-    is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
+    if (!test.menuResolutions) {
+      continue;
+    }
+
+
+    // Test icon sizes in the menu panel.
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_PANEL);
+
+    yield showBrowserAction(extension);
+    browserActionButton = browserActionWidget.forWindow(window).node;
 
-    let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
-    is(isLegacy, legacy, "Legacy class should be present?");
+    for (let resolution of Object.keys(test.menuResolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
+
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.menuResolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`);
 
-    let pageActionImage = document.getElementById(pageActionId);
-    is(pageActionImage.src, imageURL, "page action has the correct image");
+      yield SpecialPowers.popPrefEnv();
+    }
+
+    yield closeBrowserAction(extension);
+
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_NAVBAR);
   }
 
   yield extension.unload();
 });
 
 // Test that an error is thrown when providing invalid icon sizes
 add_task(function* testInvalidIconSizes() {
   let extension = ExtensionTestUtils.loadExtension({
@@ -336,31 +402,36 @@ add_task(function* testDefaultDetails() 
       background: function() {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
           let tabId = tabs[0].id;
 
           browser.pageAction.show(tabId).then(() => {
             browser.test.sendMessage("ready");
           });
         });
-      }
+      },
+
+      files: {
+        "foo/bar.png": imageBuffer,
+        "baz/quux.png": imageBuffer,
+      },
     });
 
     yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
     let browserActionId = makeWidgetId(extension.id) + "-browser-action";
     let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
     let browserActionButton = document.getElementById(browserActionId);
-    let image = browserActionButton.getAttribute("image");
+    let image = getListStyleImage(browserActionButton);
 
     ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
 
     let pageActionImage = document.getElementById(pageActionId);
-    image = pageActionImage.src;
+    image = getListStyleImage(pageActionImage);
 
     ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
 
     yield extension.unload();
 
     let node = document.getElementById(pageActionId);
     is(node, null, "pageAction image removed from document");
   }
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -93,17 +93,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let image = currentWindow.document.getElementById(pageActionId);
     if (details == null) {
       ok(image == null || image.hidden, "image is hidden");
     } else {
       ok(image, "image exists");
 
-      is(image.src, details.icon, "icon URL is correct");
+      is(getListStyleImage(image), details.icon, "icon URL is correct");
 
       let title = details.title || options.manifest.name;
       is(image.getAttribute("tooltiptext"), title, "image title is correct");
       is(image.getAttribute("aria-label"), title, "image aria-label is correct");
       // TODO: Popup URL.
     }
   }
 
@@ -172,16 +172,20 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
@@ -304,16 +308,20 @@ add_task(function* testDefaultTitle() {
 
       "page_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -5,16 +5,17 @@
 /* exported CustomizableUI makeWidgetId focusWindow forceGC
  *          getBrowserActionWidget
  *          clickBrowserAction clickPageAction
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
  *          promisePopupShown promisePopupHidden
  *          openContextMenu closeContextMenu
  *          openExtensionContextMenu closeExtensionContextMenu
+ *          imageBuffer getListStyleImage
  */
 
 var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
 var {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm");
 
 // Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
 // times in debug builds, which results in intermittent timeouts. Until we have
 // a better solution, we force a GC after certain strategic tests, which tend to
@@ -42,16 +43,27 @@ var focusWindow = Task.async(function* f
       resolve();
     }, true);
   });
 
   win.focus();
   yield promise;
 });
 
+let img = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = Uint8Array.from(atob(img), byte => byte.charCodeAt(0)).buffer;
+
+function getListStyleImage(button) {
+  let style = button.ownerDocument.defaultView.getComputedStyle(button);
+
+  let match = /^url\("(.*)"\)$/.exec(style.listStyleImage);
+
+  return match && match[1];
+}
+
 function promisePopupShown(popup) {
   return new Promise(resolve => {
     if (popup.state == "open") {
       resolve();
     } else {
       let onPopupShown = event => {
         popup.removeEventListener("popupshown", onPopupShown);
         resolve();
@@ -79,25 +91,31 @@ function getBrowserActionPopup(extension
   let group = getBrowserActionWidget(extension);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     return win.document.getElementById("customizationui-widget-panel");
   }
   return win.PanelUI.panel;
 }
 
-var clickBrowserAction = Task.async(function* (extension, win = window) {
+var showBrowserAction = Task.async(function* (extension, win = window) {
   let group = getBrowserActionWidget(extension);
   let widget = group.forWindow(win);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     ok(!widget.overflowed, "Expect widget not to be overflowed");
   } else if (group.areaType == CustomizableUI.TYPE_MENU_PANEL) {
     yield win.PanelUI.show();
   }
+});
+
+var clickBrowserAction = Task.async(function* (extension, win = window) {
+  yield showBrowserAction(extension, win);
+
+  let widget = getBrowserActionWidget(extension).forWindow(win);
 
   EventUtils.synthesizeMouseAtCenter(widget.node, {}, win);
 });
 
 function closeBrowserAction(extension, win = window) {
   let group = getBrowserActionWidget(extension);
 
   let node = win.document.getElementById(group.viewId);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1891,18 +1891,20 @@ BrowserGlue.prototype = {
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _showSyncStartedDoorhanger: function () {
     let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+    let productName = gBrandBundle.GetStringFromName("brandShortName");
     let title = bundle.GetStringFromName("syncStartNotification.title");
-    let body = bundle.GetStringFromName("syncStartNotification.body");
+    let body = bundle.formatStringFromName("syncStartNotification.body2",
+                                            [productName], 1);
 
     let clickCallback = (subject, topic, data) => {
       if (topic != "alertclickcallback")
         return;
       this._openPreferences("sync");
     }
     AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
   },
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -120,17 +120,17 @@ const CLOSED_MESSAGES = new Set([
   "SessionStore:update",
 
   // For a description see above.
   "SessionStore:error",
 ]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
-  "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
+  "TabOpen", "TabBrowserCreated", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
 const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
@@ -889,17 +889,20 @@ var SessionStoreInternal = {
   /**
    * Implement nsIDOMEventListener for handling various window and tab events
    */
   handleEvent: function ssi_handleEvent(aEvent) {
     let win = aEvent.currentTarget.ownerDocument.defaultView;
     let target = aEvent.originalTarget;
     switch (aEvent.type) {
       case "TabOpen":
-        this.onTabAdd(win, target);
+        this.onTabAdd(win);
+        break;
+      case "TabBrowserCreated":
+        this.onTabBrowserCreated(win, target);
         break;
       case "TabClose":
         // `adoptedBy` will be set if the tab was closed because it is being
         // moved to a new window.
         if (!aEvent.detail.adoptedBy)
           this.onTabClose(win, target);
         this.onTabRemove(win, target);
         break;
@@ -981,17 +984,17 @@ var SessionStoreInternal = {
       this._windows[aWindow.__SSi]._restoring = true;
     if (!aWindow.toolbar.visible)
       this._windows[aWindow.__SSi].isPopup = true;
 
     let tabbrowser = aWindow.gBrowser;
 
     // add tab change listeners to all already existing tabs
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
-      this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
+      this.onTabBrowserCreated(aWindow, tabbrowser.tabs[i]);
     }
     // notification of tab add/remove/selection/show/hide
     TAB_EVENTS.forEach(function(aEvent) {
       tabbrowser.tabContainer.addEventListener(aEvent, this, true);
     }, this);
 
     // Keep track of a browser's latest frameLoader.
     aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
@@ -1675,36 +1678,39 @@ var SessionStoreInternal = {
       case "sessionstore.max_windows_undo":
         this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
         this._capClosedWindows();
         break;
     }
   },
 
   /**
+   * save state when new tab is added
+   * @param aWindow
+   *        Window reference
+   */
+  onTabAdd: function ssi_onTabAdd(aWindow) {
+    this.saveStateDelayed(aWindow);
+  },
+
+  /**
    * set up listeners for a new tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
-   * @param aNoNotification
-   *        bool Do not save state if we're updating an existing tab
    */
-  onTabAdd: function ssi_onTabAdd(aWindow, aTab, aNoNotification) {
+  onTabBrowserCreated: function ssi_onTabBrowserCreated(aWindow, aTab) {
     let browser = aTab.linkedBrowser;
     browser.addEventListener("SwapDocShells", this);
     browser.addEventListener("oop-browser-crashed", this);
 
     if (browser.frameLoader) {
       this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
     }
-
-    if (!aNoNotification) {
-      this.saveStateDelayed(aWindow);
-    }
   },
 
   /**
    * remove listeners for a tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
--- a/browser/config/tooltool-manifests/win32/releng.manifest
+++ b/browser/config/tooltool-manifests/win32/releng.manifest
@@ -1,19 +1,19 @@
 [
 {
 "size": 266240,
 "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869",
 "algorithm": "sha512",
 "filename": "mozmake.exe"
 },
 {
-"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)",
-"size": 82463178,
-"digest": "a3c54c6792e75d53ec79caf958db25b651fcf968a37b00949fb327c54a54cad6305a4af302f267082d86d70fcf837ed0f273f85b97706c20b957ff3690889b40",
+"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)",
+"size": 88820579,
+"digest": "3bc772d951bf90b01cdba9dcd0e1d131a98519dff0710bb219784ea43d4d001dbce191071a4b3824933386bb9613f173760c438939eb396b0e0dfdad9a42e4f0",
 "algorithm": "sha512",
 "filename": "rustc.tar.bz2",
 "unpack": true
 },
 {
 "size": 167175,
 "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
 "algorithm": "sha512",
--- a/browser/config/tooltool-manifests/win64/releng.manifest
+++ b/browser/config/tooltool-manifests/win64/releng.manifest
@@ -1,19 +1,19 @@
 [
 {
 "size": 266240,
 "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869",
 "algorithm": "sha512",
 "filename": "mozmake.exe"
 },
 {
-"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)",
-"size": 88486080,
-"digest": "a4fb99cd637b236a9c30e111757ca560bc8df1b143324c1d9ab58c32470b9b9a0598e3e0d220278ee157959dcd88421496388e2ed856e6261d9c81f18e6310e9",
+"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)",
+"size": 94067220,
+"digest": "05cabda2a28ce6674f062aab589b4b3758e0cd4a4af364bb9a2e736254baa10d668936b2b7ed0df530c7f5ba8ea1e7f51ff3affc84a6551c46188b2f67f10e05",
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "rustc.tar.bz2",
 "unpack": true
 },
 {
 "size": 167175,
 "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.322
+Current extension version is: 1.5.337
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.322';
-var pdfjsBuild = 'b6826a4';
+var pdfjsVersion = '1.5.337';
+var pdfjsBuild = '11381cd';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -1217,20 +1217,17 @@ var StatTimer = (function StatTimerClosu
   };
   return StatTimer;
 })();
 
 var createBlob = function createBlob(data, contentType) {
   if (typeof Blob !== 'undefined') {
     return new Blob([data], { type: contentType });
   }
-  // Blob builder is deprecated in FF14 and removed in FF18.
-  var bb = new MozBlobBuilder();
-  bb.append(data);
-  return bb.getBlob(contentType);
+  warn('The "Blob" constructor is not supported.');
 };
 
 var createObjectURL = (function createObjectURLClosure() {
   // Blob/createObjectURL is not available, falling back to data schema.
   var digits =
     'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
   return function createObjectURL(data, contentType, forceDataSchema) {
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.322';
-var pdfjsBuild = 'b6826a4';
+var pdfjsVersion = '1.5.337';
+var pdfjsBuild = '11381cd';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -3238,20 +3238,17 @@ var StatTimer = (function StatTimerClosu
   };
   return StatTimer;
 })();
 
 var createBlob = function createBlob(data, contentType) {
   if (typeof Blob !== 'undefined') {
     return new Blob([data], { type: contentType });
   }
-  // Blob builder is deprecated in FF14 and removed in FF18.
-  var bb = new MozBlobBuilder();
-  bb.append(data);
-  return bb.getBlob(contentType);
+  warn('The "Blob" constructor is not supported.');
 };
 
 var createObjectURL = (function createObjectURLClosure() {
   // Blob/createObjectURL is not available, falling back to data schema.
   var digits =
     'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
   return function createObjectURL(data, contentType, forceDataSchema) {
@@ -27031,17 +27028,17 @@ function recoverGlyphName(name, glyphsUn
   var unicode = getUnicodeForGlyph(name, glyphsUnicodeMap);
   if (unicode !== -1) {
     for (var key in glyphsUnicodeMap) {
       if (glyphsUnicodeMap[key] === unicode) {
         return key;
       }
     }
   }
-  warn('Unable to recover a standard glyph name for: ' + name);
+  info('Unable to recover a standard glyph name for: ' + name);
   return name;
 }
 
 var Glyph = (function GlyphClosure() {
   function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId,
                  isSpace, isInFont) {
     this.fontChar = fontChar;
     this.unicode = unicode;
index eb5ccb5ec3cdf7f20c344483958a2684e11e6df5..12bae83a910277a30f57e8e30a5a4ed2f17e2c37
GIT binary patch
literal 2417
zc$}S<`9Bkk1IOnaW{$8iGq>4>OiamK)66yGYVJF7geJondK^pQiyV=V%6;W3=Bm*1
zLLr${&Jgh+PjWs_&%f~fyk76m``6EpAEqtY3@iW>0001BbCT)h-!}RWKCa*Wpd;z~
zZ#CFjIb8Z}H8nL!Nl7g&Ee#C~adB~FWo0-VuC1+YXlRH;B4IF?rlzL6yu7TeEDD9f
z;c$wIiWe?i0E597FJ6Q|Ab339*w~mzB#MfPT3A?+NF-rlVGsxe1OlzBtoZr)O-)U?
zxw*+?va_?Zot>SBhX*Gor=z2zo12@xy*-6O@$&NW@$m@?3JMJk_4D(Kh=>RX2)KUz
z`pug+X*61JaBxaW3YAKYjg3Wa%={h<(4$@^+YA5SN$@C>1pxTYo0}Rt&^S^Gc=Gp1
zXKpDLjgL5{R-`iW*G{3cW?ZAl?Pn&3mE2V=aX~<X9-{``hy;ZRF>)}6(Fo0Q^RkVO
z#RwKhjTRXqs2}d>zRKIZHj62wbw`yC)xSSvXNE}?C^$cdKeUDs?p?Y!PSw66<ckBP
zb9DN>4)41*r72GWz+-?RgDcHrf<J$_8$fEg96LZ3QbAXYPF4_00^}RIKr}A09Bg48
zB)pm<Rt(hN6cKp$gb~)z9O!2p8y8tmJ0h@p9S~9KEYs@?0zS8PtHmYG%GO=Ld_HK?
z@29zzTE4lj!BOsjoaZUB8BMoi#S$kCA2n=?@^Vdx&>Clt-qI^(r9aE}-BB`HSdbu#
zjFgq%^1XXo9Jxk6Uqr!HvTsaxZq~fUPkmhJyJGM3pg@ZB)Eg3o8O(9D6>4Kn$25k(
zYcwK+9tVgb3Tq77jM9!4RpJrmpYw8D>&ggQN=a@@&`8TWwys>-j?$oUjMI>PXOltk
z#4v~&&XvonrGCsT9ox|K40LZ|z!Eqdx4-TvtsAS>SiH59S$a%OnQZvB>|F`?A9r5s
z(ASdG!GX5WV<{=^k`8+5KRSy5xBVxJnThl37|G*(mIW|0X{rv0?KE0AK+|Wvot#%K
z%UC`9u4a11XW)K*_gJ%w0+tjf&;U4T<9X%l^s<u3Be!kv{9JdHO-(@0vO&J?heQk-
z>$;FV6!-HG!OxLUTHxV>L}oZeJh0P$08+sSZqS}Vnm;9n0!C9MdVQ#>3f5Q8LQ+&_
zb26g35M=jU0le8>rBpAktROo0iYo^!irpG>_nbmD>Z)Tf@f#xcVR)6Gg8ydc1>Eh4
zH7lP`*(<6I%GFhXxL|P&|C;NHPHziEVoOy-6<SoLdJ=cZ_@MHoO9WG{$^pK&clU}v
z4b(m2K~3Ha|0v84zUoMg9#VPKh{+js+<u<>O{eTolA={p@;DjavkMeFlXdn>sYT+B
zC)2cUUQltM+$C~7ymNxN)GgT5m@xAY(=++B<c=h(wdP4^)8Dm1tp_$7Sz=HM?;mMR
z6ia=Ly~_FOyo~es{f0i9$>vAOl`Y|kVeWHoN>;)-$+;FYGB3g&EA!4Fj1-#^NVyx;
zo)ml!XW_4dh;ykZTXA>Z$Y*kB2iAM<6Eho?om#ElhR?U0isWA8DOa2rR3GxUd39M%
zWW5qnQZ9yX)xuuF#p3nHl83DAg!8lRlthfP!|J6uPtT3aT=)@SL3mlsZD2L>#(xbR
z(VJce_$-<mAM}1+#GMBp)BWc+r5l%`jM2^20s|GSDjS0m_`5H6mJMVkW|@j6Z73m|
z^hHj6jVlG)sMWC?1~mOI9km#gs6i&on9#ir&rTv=lPm~(Rqf>soHn7ngOI;6t)bJW
zAN>As-|%ed;NZgzXX!6abR{&xgx<6er)ym6v5=8&U#~>4CzSrWDeqw^5vrHZWY;q_
zD@4h9LxU6A>6n&lft5ASdGDSK9!OqQpNRS%i5s=Er2oakl32J$u3mMaYwsV0^Qi#j
zJ_iLQ;eB^B;M*R5r=sWwW)~O#&R5i=H6EtbRzK{-L+aNg&=@yonM|JJ-SGjKcEq<~
zMLN|G*{>dBW6y`q$l78QBYuff865tF-%Q%AM>%hsxzDLcW}bjl7xvkgmu}u&725X-
z5A^IPouCXK?z0ml;(q-p*DEckmaqBaq<4>6;qG-0ljcX>iN4i8*9t0f_H;QVE100G
z2K=Ex|E!g%vmZ1U_1mimCJmJ_1Bcrx>{USp!KmQ)FLl5_*-Wg<<h{P^AiGFg--ojf
zQQ>1EBB9AiS^Nykbdf10JH)rFe4(XdOI?0Gw}(F2VGkGe<@F-+VI^Hg=JwQ&B@m9@
zA=%qkGJ9c8hc65^87V_ox0BwkuQx_TtKm&6n4NrTsNMRa>D~`A-Q{WeV;f>pj}JBz
z7ocH!oY&WO_~YlXr|mRrvZx7x?-d9g2%p*A`0L|FPKf6((1#d`2STgg2IQx6Het&4
z2$K3%JVmKzPW9*M36aCBL_&LdMWxlM3G<|g08Fb!`932^T#hjC92cPlySV)vE9D6{
zlecWv{n|xkRNit}{sQU}EGA*;m0a}p42R^-^)Q8=?u7P?(-*aJ(s6k!jEohXuP`U^
z(fjosPbmb`uJ`VbmPQL@I0}D$C&qsoNKBI?N^5xye=^(aAfY!p8aTO*H$9=ai$o#a
zn>?lWpViggKGd>i*TP?cjbHmq>@jvAHKgah?aM69U6;A7=6>@ES%mo9M+YAywyc@n
zP7PT;C|Ub}eMFK-@q4VFkC?}woomcAB>s@HK0*d_;R@f6)(vcGbF<M_P)c#ZSboEp
z9CjXQx7XZYk$pQ4co(YLC$hmDw|Vt)6~2BOQpGDhwF*DX_4U#%nh@;fvCA=6><X(6
ztz7qznQ{Y1ckYzJEn^1xCFKc@f<`OXLfVs`V>x0a1J|~xn0QWio#?%GaBmD*9k9$f
z2QAR^?*9d9EmytYy*S$4fur{d4KE!<Wvi(jXKx8Q1ELpi<csp*nA2u)>_9BRt;s{@
z9PbxkR_wqjLT(p1b$oPS9>t%~*S`dWEA{d5HvO1s=h2f@XXU)}*nX|rBsOQ!t6F1I
z67lp|+`<GOjdUaUpPj6j){s_48h1SF*PHc#^Qt9EV~vEki~hsdPYzuC0>V51b@y4K
zm~KTLcOYqo6Ya#E<$%mHQ~da1(&H<v5Vxi846)7_T<Wm*P)sy**{#iI>2rN2_d(T<
Z7%JA_k&jlwe*aw&^GjsY1{1I3e*tEYGdlnP
index 306eb43b86861ffd83ab17541d64a4e8d240c86a..e50ca4eee46e25d92e06847004f323d616237f85
GIT binary patch
literal 107
zc%17D@N?(olHy`uVBq!ia0vp^>_9BQ!3HF6HKu+5QbwLGjv*C{$r2tP{{OG9Q;1_&
zbWt^;p+Z-{zWI{QiAmlEggExJ)R+qxP213Hu-B-8fx+-v^k(la_imtW22WQ%mvv4F
FO#tGFAX@+c
index b979e523e742027645e980d8a27c54d0060ebf21..4a5e2b8a3775facabbf7e93d8b9b1b30297632be
GIT binary patch
literal 859
zc$@)S1El<kP)<h;3K|Lk000e1NJLTq001BW001Be1ONa4*>kdg0009bNkl<Zcmdta
zLxAM~0E6MIZQHhO+qP}nwr$(C{l>O!qs?nO>`2G`bN+(=Af|T(S(@31n57R7<0pDb
zv*kV(PVYe$9;O?e1ONJb6Yc5BKP*8IY3C>D9Hdz{87<JX-$0k=bQGU6bIJ$Z_=Fv!
z`3X7&{u$mI`5>30PBZ^sbmXX%4tmg@4vsUTxntmOiRVB}nLn<lUGS4&B+ErR+Sos+
zQ(of7=@9s9L@(s{fOs27n{fSbB*$%OZGXRXqYqhy<FpI<F;wD?P;COO1HK0!Dc+sd
zv~;*Da~`8@z;~Gws$~g%LQA8=Mo5ZV(Zb<QnT8TSO6!1cu{;-Nu@ONqldzLkVVVg|
zCLj?PEotU(Tj57&74RjFi??axw@&8VL8}OZ9TSlaQ1fo01x@U3Df}=k0zMDx8N-EJ
zH1~Zwx}n6k(^AmeHw^_KrQ$TBk=;$13x{bQ@G&00NmJi9-8<LG&TTXo^iRw}K|p=b
zRWzlc{dJl15KRL<42t77XyW@y;ah1Y=$@L3!hrgq6OE~Ff2~ZzjhVTR#sTl*@#{45
zec4&zG!ZqY=Aa0mJ}53~sB3q%OhaU1XBw&aH5&Ln@6rj`gjF;aHRtE6I1nap4RwOw
zHEN1XzVO{n&5b#+{3`W*pUIrdXvDu)7NZ!{h0A-(zh5>Yi#??6;a}ZB3eP57p|1DS
zXl@|>xwZ%;kT8Pz`Hz3Db$te9qxmP|1)H#*aDh5Lj}^X@`uu%;NfI2egk?GY;;*~a
z@<CZq%hUWrBO3Fc=Y54Qp&ozUUx|`Ps^wUoPyEgw7IpF>dUdWZu{mdd@#VUTEfucA
z9}iccG?Fc|>vewjnSi)TqAu#895Qn;HT?c?WeWVQy!reZ_MEv1^(6j}Uth0B86;mF
zdF6c{@Vju11>@nYT*bfq@_HT0Bh@l39~}OgKg2Iw!R~CvPMjyc=5Ky}tMIgxcku}S
zhy2W6{6`&XB>Y8u|6sYo(^$Sr?Vhc2yC#0%M}7=@X>@SpAe2G6%8YVoT%t?Q{>}Sj
lZHt=7g$$J$Igkx$P79l;1h5EfFlGP%002ovPDHLkV1kE3p%MT9
index fb7db9383669cf734685ca7422bbab1032f486fd..a0208b41377f5e006b90f4c6f6432f0156c7cd0c
GIT binary patch
literal 219
zc$@*-03`p3P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80001@Nkl<ZcmeH}
zM;d`a5Cxxb7IxAJoR<S|7uiZ?CDTTH+evcQ_tmcJ&tah45s{j4W@$ZEp9CowSC-~M
zg8DSb0rqe<uup=Bqr{{C$OF&=I66u^dc6XlcL0;gq-lHk%xnuJuqQ=h+0s&Q|8XcH
zQZcqHt;Xt;AX($W(hNvYp9a~%9?pjKyaM_q)Sr>Ai1Yx4!(rbx9*stQkf2`I11D?x
VwzoOr^jH7@002ovPDHLkV1mA0TJZn?
index 1c8b9f7010cae8edd6ca3a1146c6f07f68f5931e..0496b3577555195f96498932403c030411769159
GIT binary patch
literal 143
zc%17D@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VVV*9IAr*|t6(1k5>aAfpGT9*~
zXOfab4D%87#veSpl*$zRm>xL?oHRbsHKXB%c*k}H6$4SxhZAg?^X4iGtUtb-qb;)0
qg018G<FJ+kOb#)9&#VtUWMR17`s11Z)ixoZ4Gf;HelF{r5}E*k#W1e`
index 84279368d985d74ba324ea3433b08ea5974bcc62..6ad9ebcdf5df5a2aa8d39e01371fc8a5e4663db4
GIT binary patch
literal 167
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheJWm(LkP61+ijR+2cScBc{7$GT
z;z?kCWTIflbtL_S=?Sq8a|d1v=S7Yj`3fe<QZAFsPZ)DtmTuuydc%8&?co<TNw&nQ
zW})Uc4He3pni#)ISxsYnRF@^;a$@%v{|Od*y&D}{_IkfhYS~bpSi`{YeP_|4`&u?Y
Pn;AS^{an^LB{Ts58u&Pg
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -6417,17 +6417,17 @@ var PDFViewer = (function pdfViewer() {
     },
 
     /**
      * @param val - The scale of the pages (in percent or predefined value).
      */
     set currentScaleValue(val) {
       if (!this.pdfDocument) {
         this._currentScale = isNaN(val) ? UNKNOWN_SCALE : val;
-        this._currentScaleValue = val;
+        this._currentScaleValue = val.toString();
         return;
       }
       this._setScale(val, false);
     },
 
     /**
      * @returns {number}
      */
@@ -6604,17 +6604,17 @@ var PDFViewer = (function pdfViewer() {
         presetValue: preset ? newValue : undefined
       };
       this.eventBus.dispatch('scalechanging', arg);
       this.eventBus.dispatch('scalechange', arg);
     },
 
     _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages(
         newScale, newValue, noScroll, preset) {
-      this._currentScaleValue = newValue;
+      this._currentScaleValue = newValue.toString();
 
       if (isSameScale(this._currentScale, newScale)) {
         if (preset) {
           this._setScaleDispatchEvent(newScale, newValue, true);
         }
         return;
       }
 
@@ -7499,59 +7499,47 @@ var PDFViewerApplication = {
    * @param {string|TypedArray|ArrayBuffer} file - PDF location or binary data.
    * @param {Object} args - (optional) Additional arguments for the getDocument
    *                        call, e.g. HTTP headers ('httpHeaders') or
    *                        alternative data transport ('range').
    * @returns {Promise} - Returns the promise, which is resolved when document
    *                      is opened.
    */
   open: function pdfViewOpen(file, args) {
-    var scale = 0;
-    if (arguments.length > 2 || typeof args === 'number') {
-      console.warn('Call of open() with obsolete signature.');
-      if (typeof args === 'number') {
-        scale = args; // scale argument was found
-      }
-      args = arguments[4] || null;
-      if (arguments[3] && typeof arguments[3] === 'object') {
-        // The pdfDataRangeTransport argument is present.
-        args = Object.create(args);
-        args.range = arguments[3];
-      }
-      if (typeof arguments[2] === 'string') {
-        // The password argument is present.
-        args = Object.create(args);
-        args.password = arguments[2];
-      }
-    }
-
     if (this.pdfLoadingTask) {
       // We need to destroy already opened document.
       return this.close().then(function () {
         // Reload the preferences if a document was previously opened.
         Preferences.reload();
         // ... and repeat the open() call.
         return this.open(file, args);
       }.bind(this));
     }
 
-    var parameters = Object.create(null);
+    var parameters = Object.create(null), scale;
     if (typeof file === 'string') { // URL
       this.setTitleUsingUrl(file);
       parameters.url = file;
     } else if (file && 'byteLength' in file) { // ArrayBuffer
       parameters.data = file;
     } else if (file.url && file.originalUrl) {
       this.setTitleUsingUrl(file.originalUrl);
       parameters.url = file.url;
     }
     if (args) {
       for (var prop in args) {
         parameters[prop] = args[prop];
       }
+
+      if (args.scale) {
+        scale = args.scale;
+      }
+      if (args.length) {
+        this.pdfDocumentProperties.setFileSize(args.length);
+      }
     }
 
     var self = this;
     self.downloadComplete = false;
 
     var loadingTask = pdfjsLib.getDocument(parameters);
     this.pdfLoadingTask = loadingTask;
 
@@ -7562,17 +7550,17 @@ var PDFViewerApplication = {
 
     loadingTask.onProgress = function getDocumentProgress(progressData) {
       self.progress(progressData.loaded / progressData.total);
     };
 
     // Listen for unsupported features to trigger the fallback UI.
     loadingTask.onUnsupportedFeature = this.fallback.bind(this);
 
-    var result = loadingTask.promise.then(
+    return loadingTask.promise.then(
       function getDocumentCallback(pdfDocument) {
         self.load(pdfDocument, scale);
       },
       function getDocumentError(exception) {
         var message = exception && exception.message;
         var loadingErrorMessage = mozL10n.get('loading_error', null,
           'An error occurred while loading the PDF.');
 
@@ -7592,21 +7580,16 @@ var PDFViewerApplication = {
         var moreInfo = {
           message: message
         };
         self.error(loadingErrorMessage, moreInfo);
 
         throw new Error(loadingErrorMessage);
       }
     );
-
-    if (args && args.length) {
-      PDFViewerApplication.pdfDocumentProperties.setFileSize(args.length);
-    }
-    return result;
   },
 
   download: function pdfViewDownload() {
     function downloadByUrl() {
       downloadManager.downloadUrl(url, filename);
     }
 
     var url = this.url.split('#')[0];
@@ -7728,21 +7711,19 @@ var PDFViewerApplication = {
 
     this.pdfDocumentProperties.setDocumentAndUrl(pdfDocument, this.url);
 
     var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
       self.downloadComplete = true;
       self.loadingBar.hide();
     });
 
-    var pagesCount = pdfDocument.numPages;
-    var toolbarConfig = this.appConfig.toolbar;
-    toolbarConfig.numPages.textContent =
-      mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
-    toolbarConfig.pageNumber.max = pagesCount;
+    this._updateUIToolbar({
+      resetNumPages: true,
+    });
 
     var id = this.documentFingerprint = pdfDocument.fingerprint;
     var store = this.store = new ViewHistory(id);
 
     var baseDocumentUrl = this.url.split('#')[0];
     this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
 
     var pdfViewer = this.pdfViewer;
@@ -7930,20 +7911,16 @@ var PDFViewerApplication = {
   },
 
   setInitialView: function pdfViewSetInitialView(storedHash, options) {
     var scale = options && options.scale;
     var sidebarView = options && options.sidebarView;
 
     this.isInitialViewSet = true;
 
-    // When opening a new file, when one is already loaded in the viewer,
-    // ensure that the 'pageNumber' element displays the correct value.
-    this.appConfig.toolbar.pageNumber.value = this.pdfViewer.currentPageNumber;
-
     this.pdfSidebar.setInitialView(this.preferenceSidebarViewOnLoad ||
                                    (sidebarView | 0));
 
     if (this.initialDestination) {
       this.pdfLinkService.navigateTo(this.initialDestination);
       this.initialDestination = null;
     } else if (this.initialBookmark) {
       this.pdfLinkService.setHash(this.initialBookmark);
@@ -8097,16 +8074,77 @@ var PDFViewerApplication = {
    */
   scrollPresentationMode: function pdfViewScrollPresentationMode(delta) {
     if (!this.pdfPresentationMode) {
       return;
     }
     this.pdfPresentationMode.mouseScroll(delta);
   },
 
+  /**
+   * @typedef UpdateUIToolbarParameters
+   * @property {number} pageNumber
+   * @property {string} scaleValue
+   * @property {scale} scale
+   * @property {boolean} resetNumPages
+   */
+
+  /**
+   * @param {Object} UpdateUIToolbarParameters
+   * @private
+   */
+  _updateUIToolbar: function (params) {
+    function selectScaleOption(value, scale) {
+      var options = toolbarConfig.scaleSelect.options;
+      var predefinedValueFound = false;
+      for (var i = 0, ii = options.length; i < ii; i++) {
+        var option = options[i];
+        if (option.value !== value) {
+          option.selected = false;
+          continue;
+        }
+        option.selected = true;
+        predefinedValueFound = true;
+      }
+      if (!predefinedValueFound) {
+        var customScale = Math.round(scale * 10000) / 100;
+        toolbarConfig.customScaleOption.textContent =
+          mozL10n.get('page_scale_percent', {scale: customScale}, '{{scale}}%');
+        toolbarConfig.customScaleOption.selected = true;
+      }
+    }
+
+    var pageNumber = params.pageNumber || this.pdfViewer.currentPageNumber;
+    var scaleValue = (params.scaleValue || params.scale ||
+      this.pdfViewer.currentScaleValue || DEFAULT_SCALE_VALUE).toString();
+    var scale = params.scale || this.pdfViewer.currentScale;
+    var resetNumPages = params.resetNumPages || false;
+
+    var toolbarConfig = this.appConfig.toolbar;
+    var pagesCount = this.pagesCount;
+
+    if (resetNumPages) {
+      toolbarConfig.numPages.textContent =
+        mozL10n.get('page_of', { pageCount: pagesCount }, 'of {{pageCount}}');
+      toolbarConfig.pageNumber.max = pagesCount;
+    }
+    toolbarConfig.pageNumber.value = pageNumber;
+
+    toolbarConfig.previous.disabled = (pageNumber <= 1);
+    toolbarConfig.next.disabled = (pageNumber >= pagesCount);
+
+    toolbarConfig.firstPage.disabled = (pageNumber <= 1);
+    toolbarConfig.lastPage.disabled = (pageNumber >= pagesCount);
+
+    toolbarConfig.zoomOut.disabled = (scale === MIN_SCALE);
+    toolbarConfig.zoomIn.disabled = (scale === MAX_SCALE);
+
+    selectScaleOption(scaleValue, scale);
+  },
+
   bindEvents: function pdfViewBindEvents() {
     var eventBus = this.eventBus;
 
     eventBus.on('resize', webViewerResize);
     eventBus.on('localized', webViewerLocalized);
     eventBus.on('hashchange', webViewerHashchange);
     eventBus.on('beforeprint', this.beforePrint.bind(this));
     eventBus.on('afterprint', this.afterPrint.bind(this));
@@ -8532,31 +8570,16 @@ function webViewerHashchange(e) {
       PDFViewerApplication.initialBookmark = hash;
     } else {
       PDFViewerApplication.pdfLinkService.setHash(hash);
     }
   }
 }
 
 
-function selectScaleOption(value) {
-  var options = PDFViewerApplication.appConfig.toolbar.scaleSelect.options;
-  var predefinedValueFound = false;
-  for (var i = 0, ii = options.length; i < ii; i++) {
-    var option = options[i];
-    if (option.value !== value) {
-      option.selected = false;
-      continue;
-    }
-    option.selected = true;
-    predefinedValueFound = true;
-  }
-  return predefinedValueFound;
-}
-
 window.addEventListener('localized', function localized(evt) {
   PDFViewerApplication.eventBus.dispatch('localized');
 });
 
 function webViewerLocalized() {
   document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
 
   PDFViewerApplication.animationStartedPromise.then(function() {
@@ -8632,52 +8655,37 @@ function webViewerFindFromUrlHash(e) {
     phraseSearch: e.phraseSearch,
     caseSensitive: false,
     highlightAll: true,
     findPrevious: false
   });
 }
 
 function webViewerScaleChanging(e) {
-  var appConfig = PDFViewerApplication.appConfig;
-  appConfig.toolbar.zoomOut.disabled = (e.scale === MIN_SCALE);
-  appConfig.toolbar.zoomIn.disabled = (e.scale === MAX_SCALE);
-
-  // Update the 'scaleSelect' DOM element.
-  var predefinedValueFound = selectScaleOption(e.presetValue ||
-                                               '' + e.scale);
-  if (!predefinedValueFound) {
-    var customScaleOption = appConfig.toolbar.customScaleOption;
-    var customScale = Math.round(e.scale * 10000) / 100;
-    customScaleOption.textContent =
-      mozL10n.get('page_scale_percent', { scale: customScale }, '{{scale}}%');
-    customScaleOption.selected = true;
-  }
+  PDFViewerApplication._updateUIToolbar({
+    scaleValue: e.presetValue,
+    scale: e.scale,
+  });
+
   if (!PDFViewerApplication.initialized) {
     return;
   }
   PDFViewerApplication.pdfViewer.update();
 }
 
 function webViewerPageChanging(e) {
   var page = e.pageNumber;
-  if (e.previousPageNumber !== page) {
-    PDFViewerApplication.appConfig.toolbar.pageNumber.value = page;
-
-    if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
-      PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
-    }
-  }
-  var numPages = PDFViewerApplication.pagesCount;
-
-  PDFViewerApplication.appConfig.toolbar.previous.disabled = (page <= 1);
-  PDFViewerApplication.appConfig.toolbar.next.disabled = (page >= numPages);
-
-  PDFViewerApplication.appConfig.toolbar.firstPage.disabled = (page <= 1);
-  PDFViewerApplication.appConfig.toolbar.lastPage.disabled = (page >= numPages);
+
+  PDFViewerApplication._updateUIToolbar({
+    pageNumber: page,
+  });
+  if (e.previousPageNumber !== page &&
+      PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
+    PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
+  }
 
   // we need to update stats
   if (pdfjsLib.PDFJS.pdfBug && Stats.enabled) {
     var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1);
     if (pageView.stats) {
       Stats.add(page, pageView.stats);
     }
   }
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -22,14 +22,19 @@ verificationSentTitle = Verification Sen
 # LOCALIZATION NOTE (verificationSentBody) - %S = Email address of user's Firefox Account
 verificationSentBody = A verification link has been sent to %S.
 verificationNotSentTitle = Unable to Send Verification
 verificationNotSentBody = We are unable to send a verification mail at this time, please try again later.
 
 # LOCALIZATION NOTE (syncStartNotification.title, syncStartNotification.body)
 # These strings are used in a notification shown after Sync is connected.
 syncStartNotification.title = Sync enabled
-syncStartNotification.body = Firefox will begin syncing momentarily.
+# %S is brandShortName
+syncStartNotification.body2 = %S will begin syncing momentarily.
 
 # LOCALIZATION NOTE (deviceDisconnectedNotification.title, deviceDisconnectedNotification.body)
 # These strings are used in a notification shown after Sync was disconnected remotely.
 deviceDisconnectedNotification.title = Sync disconnected
 deviceDisconnectedNotification.body = This computer has been successfully disconnected from Firefox Sync.
+
+# LOCALIZATION NOTE (sendTabToAllDevices.menuitem)
+# Displayed in the Send Tabs context menu when right clicking a tab, a page or a link.
+sendTabToAllDevices.menuitem = All Devices
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -35,16 +35,22 @@ left instead of right. -->
 used as a metaphor for expressing the fact that these tabs are "pinned" to the
 left edge of the tabstrip. Really we just want the string to express the idea
 that this is a lightweight and reversible action that keeps your tab where you
 can reach it easily. -->
 <!ENTITY  pinTab.label                       "Pin Tab">
 <!ENTITY  pinTab.accesskey                   "P">
 <!ENTITY  unpinTab.label                     "Unpin Tab">
 <!ENTITY  unpinTab.accesskey                 "b">
+<!ENTITY  sendTabToDevice.label              "Send Tab to Device">
+<!ENTITY  sendTabToDevice.accesskey          "D">
+<!ENTITY  sendPageToDevice.label             "Send Page to Device">
+<!ENTITY  sendPageToDevice.accesskey         "D">
+<!ENTITY  sendLinkToDevice.label             "Send Link to Device">
+<!ENTITY  sendLinkToDevice.accesskey         "D">
 <!ENTITY  moveToNewWindow.label              "Move to New Window">
 <!ENTITY  moveToNewWindow.accesskey          "W">
 <!ENTITY  bookmarkAllTabs.label              "Bookmark All Tabs…">
 <!ENTITY  bookmarkAllTabs.accesskey          "T">
 <!ENTITY  undoCloseTab.label                 "Undo Close Tab">
 <!ENTITY  undoCloseTab.accesskey             "U">
 <!ENTITY  closeTab.label                     "Close Tab">
 <!ENTITY  closeTab.accesskey                 "c">
--- a/browser/modules/NetworkPrioritizer.jsm
+++ b/browser/modules/NetworkPrioritizer.jsm
@@ -22,17 +22,17 @@ Components.utils.import("resource://gre/
 
 // Lazy getters
 XPCOMUtils.defineLazyServiceGetter(this, "_focusManager",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
 
 
 // Constants
-const TAB_EVENTS = ["TabOpen", "TabSelect", "TabRemotenessChange"];
+const TAB_EVENTS = ["TabBrowserCreated", "TabSelect", "TabRemotenessChange"];
 const WINDOW_EVENTS = ["activate", "unload"];
 // lower value means higher priority
 const PRIORITY_DELTA = Ci.nsISupportsPriority.PRIORITY_NORMAL - Ci.nsISupportsPriority.PRIORITY_LOW;
 
 
 // Variables
 var _lastFocusedWindow = null;
 var _windows = [];
@@ -44,17 +44,17 @@ var _priorityBackup = new WeakMap();
 this.trackBrowserWindow = function trackBrowserWindow(aWindow) {
   WindowHelper.addWindow(aWindow);
 }
 
 
 // Global methods
 function _handleEvent(aEvent) {
   switch (aEvent.type) {
-    case "TabOpen":
+    case "TabBrowserCreated":
       BrowserHelper.onOpen(aEvent.target.linkedBrowser);
       break;
     case "TabSelect":
       BrowserHelper.onSelect(aEvent.target.linkedBrowser);
       break;
     case "activate":
       WindowHelper.onActivate(aEvent.target);
       break;
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -132,24 +132,31 @@ def namespace(**kwargs):
 # such as `set_config`. But those functions do not take immediate values.
 # The `delayed_getattr` function allows access to attributes from the result
 # of a @depends function in a non-immediate manner.
 #   @depends('--option')
 #   def option(value)
 #       return namespace(foo=value)
 #   set_config('FOO', delayed_getattr(option, 'foo')
 @template
+@imports('__sandbox__')
 def delayed_getattr(func, key):
-    @depends(func)
-    def result(value):
+    _, deps = __sandbox__._depends.get(func, (None, ()))
+
+    def result(value, _=None):
         # The @depends function we're being passed may have returned
         # None, or an object that simply doesn't have the wanted key.
         # In that case, just return None.
         return getattr(value, key, None)
-    return result
+
+    # Automatically add a dependency on --help when the given @depends
+    # function itself depends on --help.
+    if __sandbox__._help_option in deps:
+        return depends(func, '--help')(result)
+    return depends(func)(result)
 
 
 # Like @depends, but the decorated function is only called if one of the
 # arguments it would be called with has a positive value (bool(value) is True)
 @template
 def depends_if(*args):
     def decorator(func):
         @depends(*args)
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -777,27 +777,30 @@ nsScriptSecurityManager::CheckLoadURIWit
 
     //-- Some callers do not allow loading javascript:
     if ((aFlags & nsIScriptSecurityManager::DISALLOW_SCRIPT) &&
          targetScheme.EqualsLiteral("javascript"))
     {
        return NS_ERROR_DOM_BAD_URI;
     }
 
-    NS_NAMED_LITERAL_STRING(errorTag, "CheckLoadURIError");
-    bool reportErrors = !(aFlags & nsIScriptSecurityManager::DONT_REPORT_ERRORS);
-
     // Check for uris that are only loadable by principals that subsume them
     bool hasFlags;
     rv = NS_URIChainHasFlags(targetBaseURI,
                              nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (hasFlags) {
+        // check nothing else in the URI chain has flags that prevent
+        // access:
+        rv = CheckLoadURIFlags(sourceURI, aTargetURI, sourceBaseURI,
+                               targetBaseURI, aFlags);
+        NS_ENSURE_SUCCESS(rv, rv);
+        // Check the principal is allowed to load the target.
         return aPrincipal->CheckMayLoad(targetBaseURI, true, false);
     }
 
     //-- get the source scheme
     nsAutoCString sourceScheme;
     rv = sourceBaseURI->GetScheme(sourceScheme);
     if (NS_FAILED(rv)) return rv;
 
@@ -859,35 +862,60 @@ nsScriptSecurityManager::CheckLoadURIWit
                 !nsContentUtils::IsExactSitePermAllow(aPrincipal, WEBAPPS_PERM_NAME)) {
                 return NS_ERROR_DOM_BAD_URI;
             }
         }
         return NS_OK;
     }
 
     // If the schemes don't match, the policy is specified by the protocol
-    // flags on the target URI.  Note that the order of policy checks here is
-    // very important!  We start from most restrictive and work our way down.
-    // Note that since we're working with the innermost URI, we can just use
-    // the methods that work on chains of nested URIs and they will only look
-    // at the flags for our one URI.
+    // flags on the target URI.
+    return CheckLoadURIFlags(sourceURI, aTargetURI, sourceBaseURI,
+                             targetBaseURI, aFlags);
+}
+
+/**
+ * Helper method to check whether the target URI and its innermost ("base") URI
+ * has protocol flags that should stop it from being loaded by the source URI
+ * (and/or the source URI's innermost ("base") URI), taking into account any
+ * nsIScriptSecurityManager flags originally passed to
+ * CheckLoadURIWithPrincipal and friends.
+ *
+ * @return if success, access is allowed. Otherwise, deny access
+ */
+nsresult
+nsScriptSecurityManager::CheckLoadURIFlags(nsIURI *aSourceURI,
+                                           nsIURI *aTargetURI,
+                                           nsIURI *aSourceBaseURI,
+                                           nsIURI *aTargetBaseURI,
+                                           uint32_t aFlags)
+{
+    // Note that the order of policy checks here is very important!
+    // We start from most restrictive and work our way down.
+    bool reportErrors = !(aFlags & nsIScriptSecurityManager::DONT_REPORT_ERRORS);
+    NS_NAMED_LITERAL_STRING(errorTag, "CheckLoadURIError");
+
+    nsAutoCString targetScheme;
+    nsresult rv = aTargetBaseURI->GetScheme(targetScheme);
+    if (NS_FAILED(rv)) return rv;
 
     // Check for system target URI
     rv = DenyAccessIfURIHasFlags(aTargetURI,
                                  nsIProtocolHandler::URI_DANGEROUS_TO_LOAD);
     if (NS_FAILED(rv)) {
         // Deny access, since the origin principal is not system
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return rv;
     }
 
     // Check for chrome target URI
-    rv = NS_URIChainHasFlags(targetBaseURI,
+    bool hasFlags = false;
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
                              nsIProtocolHandler::URI_IS_UI_RESOURCE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
     if (hasFlags) {
         if (aFlags & nsIScriptSecurityManager::ALLOW_CHROME) {
 
             // For now, don't change behavior for resource:// or moz-icon:// and
             // just allow them.
@@ -899,85 +927,94 @@ nsScriptSecurityManager::CheckLoadURIWit
             // target if ALLOW_CHROME is set.
             //
             // ALLOW_CHROME is a flag that we pass on all loads _except_ docshell
             // loads (since docshell loads run the loaded content with its origin
             // principal). So we're effectively allowing resource://, chrome://,
             // and moz-icon:// source URIs to load resource://, chrome://, and
             // moz-icon:// files, so long as they're not loading it as a document.
             bool sourceIsUIResource;
-            rv = NS_URIChainHasFlags(sourceBaseURI,
+            rv = NS_URIChainHasFlags(aSourceBaseURI,
                                      nsIProtocolHandler::URI_IS_UI_RESOURCE,
                                      &sourceIsUIResource);
             NS_ENSURE_SUCCESS(rv, rv);
             if (sourceIsUIResource) {
                 return NS_OK;
             }
 
             // Allow the load only if the chrome package is whitelisted.
             nsCOMPtr<nsIXULChromeRegistry> reg(do_GetService(
                                                  NS_CHROMEREGISTRY_CONTRACTID));
             if (reg) {
                 bool accessAllowed = false;
-                reg->AllowContentToAccess(targetBaseURI, &accessAllowed);
+                reg->AllowContentToAccess(aTargetBaseURI, &accessAllowed);
                 if (accessAllowed) {
                     return NS_OK;
                 }
             }
         }
 
         // Special-case the hidden window: it's allowed to load
         // URI_IS_UI_RESOURCE no matter what.  Bug 1145470 tracks removing this.
         nsAutoCString sourceSpec;
-        if (NS_SUCCEEDED(sourceBaseURI->GetSpec(sourceSpec)) &&
+        if (NS_SUCCEEDED(aSourceBaseURI->GetSpec(sourceSpec)) &&
             sourceSpec.EqualsLiteral("resource://gre-resources/hiddenWindow.html")) {
             return NS_OK;
         }
 
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return NS_ERROR_DOM_BAD_URI;
     }
 
     // Check for target URI pointing to a file
     rv = NS_URIChainHasFlags(aTargetURI,
                              nsIProtocolHandler::URI_IS_LOCAL_FILE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
     if (hasFlags) {
         // Allow domains that were whitelisted in the prefs. In 99.9% of cases,
         // this array is empty.
         for (size_t i = 0; i < mFileURIWhitelist.Length(); ++i) {
-            if (EqualOrSubdomain(sourceURI, mFileURIWhitelist[i])) {
+            if (EqualOrSubdomain(aSourceURI, mFileURIWhitelist[i])) {
                 return NS_OK;
             }
         }
 
         // Allow chrome://
-        if (sourceScheme.EqualsLiteral("chrome")) {
+        bool isChrome = false;
+        if (NS_SUCCEEDED(aSourceBaseURI->SchemeIs("chrome", &isChrome)) && isChrome) {
             return NS_OK;
         }
 
         // Nothing else.
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return NS_ERROR_DOM_BAD_URI;
     }
 
     // OK, everyone is allowed to load this, since unflagged handlers are
     // deprecated but treated as URI_LOADABLE_BY_ANYONE.  But check whether we
     // need to warn.  At some point we'll want to make this warning into an
     // error and treat unflagged handlers as URI_DANGEROUS_TO_LOAD.
-    rv = NS_URIChainHasFlags(targetBaseURI,
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
                              nsIProtocolHandler::URI_LOADABLE_BY_ANYONE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
-    if (!hasFlags) {
+    // NB: we also get here if the base URI is URI_LOADABLE_BY_SUBSUMERS,
+    // and none of the rest of the nested chain of URIs for aTargetURI
+    // prohibits the load, so avoid warning in that case:
+    bool hasSubsumersFlag = false;
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
+                             nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS,
+                             &hasSubsumersFlag);
+    NS_ENSURE_SUCCESS(rv, rv);
+    if (!hasFlags && !hasSubsumersFlag) {
         nsXPIDLString message;
         NS_ConvertASCIItoUTF16 ucsTargetScheme(targetScheme);
         const char16_t* formatStrings[] = { ucsTargetScheme.get() };
         rv = sStrBundle->
             FormatStringFromName(MOZ_UTF16("ProtocolFlagError"),
                                  formatStrings,
                                  ArrayLength(formatStrings),
                                  getter_Copies(message));
--- a/caps/nsScriptSecurityManager.h
+++ b/caps/nsScriptSecurityManager.h
@@ -115,16 +115,20 @@ private:
 
     // If aURI is a moz-extension:// URI, set mAddonId to the associated addon.
     nsresult MaybeSetAddonIdFromURI(mozilla::PrincipalOriginAttributes& aAttrs, nsIURI* aURI);
 
     nsresult GetChannelResultPrincipal(nsIChannel* aChannel,
                                        nsIPrincipal** aPrincipal,
                                        bool aIgnoreSandboxing);
 
+    nsresult
+    CheckLoadURIFlags(nsIURI* aSourceURI, nsIURI* aTargetURI, nsIURI* aSourceBaseURI,
+                      nsIURI* aTargetBaseURI, uint32_t aFlags);
+
     nsCOMPtr<nsIPrincipal> mSystemPrincipal;
     bool mPrefInitialized;
     bool mIsJavaScriptEnabled;
     nsTArray<nsCOMPtr<nsIURI>> mFileURIWhitelist;
 
     // This machinery controls new-style domain policies. The old-style
     // policy machinery will be removed soon.
     nsCOMPtr<nsIDomainPolicy> mDomainPolicy;
--- a/config/check_spidermonkey_style.py
+++ b/config/check_spidermonkey_style.py
@@ -66,16 +66,18 @@ included_inclnames_to_ignore = set([
     'jsautokw.h',               # generated in $OBJDIR
     'jscustomallocator.h',      # provided by embedders;  allowed to be missing
     'js-config.h',              # generated in $OBJDIR
     'fdlibm.h',                 # fdlibm
     'pratom.h',                 # NSPR
     'prcvar.h',                 # NSPR
     'prerror.h',                # NSPR
     'prinit.h',                 # NSPR
+    'prio.h',                   # NSPR
+    'private/pprio.h',          # NSPR
     'prlink.h',                 # NSPR
     'prlock.h',                 # NSPR
     'prprf.h',                  # NSPR
     'prthread.h',               # NSPR
     'prtypes.h',                # NSPR
     'selfhosted.out.h',         # generated in $OBJDIR
     'shellmoduleloader.out.h',  # generated in $OBJDIR
     'unicode/locid.h',          # ICU
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.css
@@ -0,0 +1,15 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.devtools-inspector-tab-frame {
+  border: none;
+  height: 100%;
+  width: 100%;
+}
+
+.devtools-inspector-tab-panel {
+  width: 100%;
+  height: 100%;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Side panel for the Inspector panel.
+ * This side panel is using an existing DOM node as a content.
+ */
+var InspectorTabPanel = createClass({
+  displayName: "InspectorTabPanel",
+
+  propTypes: {
+    onMount: PropTypes.func,
+  },
+
+  componentDidMount: function () {
+    let doc = this.refs.content.ownerDocument;
+    let panel = doc.getElementById("sidebar-panel-" + this.props.id);
+
+    // Append existing DOM node into panel's content.
+    this.refs.content.appendChild(panel);
+
+    if (this.props.onMount) {
+      this.props.onMount(this.refs.content, this.props);
+    }
+  },
+
+  componentWillUnmount: function () {
+    let doc = this.refs.content.ownerDocument;
+    let panels = doc.getElementById("tabpanels");
+
+    // Move panel's content node back into list of tab panels.
+    panels.appendChild(this.refs.content.firstChild);
+  },
+
+  render: function () {
+    return (
+      div({
+        ref: "content",
+        className: "devtools-inspector-tab-panel",
+      })
+    );
+  }
+});
+
+module.exports = InspectorTabPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'inspector-tab-panel.css',
+    'inspector-tab-panel.js',
+)
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -183,17 +183,17 @@ function CssComputedView(inspector, docu
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.element.addEventListener("click", this._onClick);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
   this.searchField.addEventListener("contextmenu",
                                     this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
-  this.includeBrowserStylesCheckbox.addEventListener("command",
+  this.includeBrowserStylesCheckbox.addEventListener("input",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
   // No results text.
   this.noResults = this.styleDocument.getElementById("computedview-no-results");
 
   // Refresh panel when color unit changed.
@@ -768,18 +768,18 @@ CssComputedView.prototype = {
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
     this.element.removeEventListener("click", this._onClick);
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
     this.searchField.removeEventListener("contextmenu",
                                          this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
-    this.includeBrowserStylesCheckbox.removeEventListener("command",
-      this.includeBrowserStylesChanged);
+    this.includeBrowserStylesCheckbox.removeEventListener("input",
+      this._onIncludeBrowserStyles);
 
     // Nodes used in templating
     this.root = null;
     this.element = null;
     this.panel = null;
     this.searchField = null;
     this.searchClearButton = null;
     this.includeBrowserStylesCheckbox = null;
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -29,17 +29,17 @@ loader.lazyRequireGetter(this, "CSS", "C
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "ComputedViewTool", "devtools/client/inspector/computed/computed", true);
 loader.lazyRequireGetter(this, "FontInspector", "devtools/client/inspector/fonts/fonts", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
 loader.lazyRequireGetter(this, "LayoutView", "devtools/client/inspector/layout/layout", true);
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup", true);
 loader.lazyRequireGetter(this, "RuleViewTool", "devtools/client/inspector/rules/rules", true);
-loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
+loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
 loader.lazyRequireGetter(this, "ViewHelpers", "devtools/client/shared/widgets/view-helpers", true);
 
 loader.lazyGetter(this, "strings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/inspector.properties");
 });
 loader.lazyGetter(this, "toolboxStrings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 });
@@ -407,42 +407,100 @@ InspectorPanel.prototype = {
 
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
     if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
        defaultTab == "fontinspector") {
       defaultTab = "ruleview";
     }
 
+    // Append all side panels
+    this.sidebar.addExistingTab(
+      "ruleview",
+      strings.GetStringFromName("inspector.sidebar.ruleViewTitle"),
+      defaultTab == "ruleview");
+
+    this.sidebar.addExistingTab(
+      "computedview",
+      strings.GetStringFromName("inspector.sidebar.computedViewTitle"),
+      defaultTab == "computedview");
+
+    this.sidebar.addExistingTab(
+      "layoutview",
+      strings.GetStringFromName("inspector.sidebar.layoutViewTitle"),
+      defaultTab == "layoutview");
+
     this._setDefaultSidebar = (event, toolId) => {
       Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
     };
 
     this.sidebar.on("select", this._setDefaultSidebar);
 
     this.ruleview = new RuleViewTool(this, this.panelWin);
     this.computedview = new ComputedViewTool(this, this.panelWin);
     this.layoutview = new LayoutView(this, this.panelWin);
 
     if (this.target.form.animationsActor) {
-      this.sidebar.addTab("animationinspector",
-                          "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
-                          {selected: defaultTab == "animationinspector",
-                           insertBefore: "fontinspector"});
+      this.sidebar.addFrameTab(
+        "animationinspector",
+        strings.GetStringFromName("inspector.sidebar.animationInspectorTitle"),
+        "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
+        defaultTab == "animationinspector");
     }
 
     if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
         this.canGetUsedFontFaces) {
+      this.sidebar.addExistingTab(
+        "fontinspector",
+        strings.GetStringFromName("inspector.sidebar.fontInspectorTitle"),
+        defaultTab == "fontinspector");
+
       this.fontInspector = new FontInspector(this, this.panelWin);
       this.sidebar.toggleTab(true, "fontinspector");
     }
 
+    this.setupSidebarToggle();
+    this.setupSidebarWidth();
+
     this.sidebar.show(defaultTab);
+  },
+
+  /**
+   * Sidebar width is currently driven by vbox.inspector-sidebar-container
+   * element, which is located at the left side of the side bar splitter.
+   * It's width is changed by the splitter and stored into preferences.
+   * As soon as bug 1260552 is fixed and new HTML based splitter in place
+   * the width can be driven by div.inspector-sidebar element. This element
+   * represents the ToolSidebar and so, the entire logic related to width
+   * persistence can be done inside the ToolSidebar.
+   */
+  setupSidebarWidth: function () {
+    let sidePaneContainer = this.panelDoc.querySelector(
+      "#inspector-sidebar-container");
 
-    this.setupSidebarToggle();
+    this.sidebar.on("show", () => {
+      try {
+        sidePaneContainer.width = Services.prefs.getIntPref(
+          "devtools.toolsidebar-width.inspector");
+      } catch (e) {
+        // The default width is the min-width set in CSS
+        // for #inspector-sidebar-container
+        sidePaneContainer.width = 450;
+      }
+    });
+
+    this.sidebar.on("hide", () => {
+      Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
+        sidePaneContainer.width);
+    });
+
+    this.sidebar.on("destroy", () => {
+      Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
+        sidePaneContainer.width);
+    });
   },
 
   /**
    * Add the expand/collapse behavior for the sidebar panel.
    */
   setupSidebarToggle: function () {
     let SidebarToggle = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/sidebar-toggle"));
@@ -1165,42 +1223,49 @@ InspectorPanel.prototype = {
     }
   },
 
   /**
    * When the pane toggle button is clicked, toggle the pane, change the button
    * state and tooltip.
    */
   onPaneToggleButtonActivated: function (e) {
-    let sidePane = this.panelDoc.querySelector("#inspector-sidebar");
+    let sidePaneContainer = this.panelDoc.querySelector("#inspector-sidebar-container");
     let isVisible = !this._sidebarToggle.state.collapsed;
+    let sidePane = this.panelDoc.querySelector(
+      "#inspector-sidebar .devtools-sidebar-tabs");
 
     // Make sure the sidebar has width and height attributes before collapsing
     // because ViewHelpers needs it.
     if (isVisible) {
-      let rect = sidePane.getBoundingClientRect();
-      if (!sidePane.hasAttribute("width")) {
-        sidePane.setAttribute("width", rect.width);
+      let rect = sidePaneContainer.getBoundingClientRect();
+      if (!sidePaneContainer.hasAttribute("width")) {
+        sidePaneContainer.setAttribute("width", rect.width);
+        sidePane.style.width = rect.width + "px";
       }
       // always refresh the height attribute before collapsing, it could have
       // been modified by resizing the container.
-      sidePane.setAttribute("height", rect.height);
+      sidePaneContainer.setAttribute("height", rect.height);
+      sidePane.style.height = rect.height + "px";
     }
 
+    let onAnimationDone = () => {
+      if (isVisible) {
+        this._sidebarToggle.setState({collapsed: true});
+      } else {
+        this._sidebarToggle.setState({collapsed: false});
+      }
+    };
+
     ViewHelpers.togglePane({
       visible: !isVisible,
       animated: true,
-      delayed: true
-    }, sidePane);
-
-    if (isVisible) {
-      this._sidebarToggle.setState({collapsed: true});
-    } else {
-      this._sidebarToggle.setState({collapsed: false});
-    }
+      delayed: true,
+      callback: onAnimationDone
+    }, sidePaneContainer);
   },
 
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
   addNode: Task.async(function* () {
     if (!this.canAddHTMLChild()) {
--- a/devtools/client/inspector/inspector.css
+++ b/devtools/client/inspector/inspector.css
@@ -1,19 +1,30 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-#inspector-sidebar {
-  min-width: 250px;
+/* Set the minimum width for the side bar so, all tabs are
+  properly visible. The value can be decreased when bug 1281789
+  is fixed and the all-tabs-menu is available again. */
+#inspector-sidebar-container {
+  overflow: hidden;
+  min-width: 450px;
+  position: relative;
 }
 
+#inspector-sidebar {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+
+/* Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css */
 .inspector-tabpanel > * {
-  /*
-   * Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css
-   */
   -moz-user-focus: normal;
 }
 
 #inspector-sidebar-toggle-box {
   line-height: initial;
 }
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -6,17 +6,22 @@
 <?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/content/inspector/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/rules.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/computed.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/fonts.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/animationinspector.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/shared/components/sidebar-toggle.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabs.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabbar.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/inspector/components/side-panel.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/inspector/components/inspector-tab-panel.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % inspectorDTD SYSTEM "chrome://devtools/locale/inspector.dtd"> %inspectorDTD;
   <!ENTITY % styleinspectorDTD SYSTEM "chrome://devtools/locale/styleinspector.dtd"> %styleinspectorDTD;
   <!ENTITY % fontinspectorDTD SYSTEM "chrome://devtools/locale/font-inspector.dtd"> %fontinspectorDTD;
   <!ENTITY % layoutviewDTD SYSTEM "chrome://devtools/locale/layoutview.dtd"> %layoutviewDTD;
 ]>
 
@@ -45,161 +50,159 @@
       </html:div>
       <vbox flex="1" id="markup-box">
       </vbox>
       <html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
         <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/>
       </html:div>
     </vbox>
     <splitter class="devtools-side-splitter"/>
-    <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs" hidden="true">
-      <tabs>
-        <tab id="sidebar-tab-ruleview"
-             label="&ruleViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-computedview"
-             label="&computedViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-layoutview"
-             label="&layoutViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-fontinspector"
-             label="&fontInspectorTitle;"
-             crop="end"
-             hidden="true"/>
-      </tabs>
-      <tabpanels flex="1">
-        <tabpanel id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div id="ruleview-toolbar-container" class="devtools-toolbar">
-            <html:div id="ruleview-toolbar">
-              <html:div class="devtools-searchbox">
-                <html:input id="ruleview-searchbox"
-                            class="devtools-filterinput devtools-rule-searchbox"
-                            type="search"
-                            placeholder="&filterStylesPlaceholder;"/>
-                <html:button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
-              </html:div>
-              <html:div id="ruleview-command-toolbar">
-                <html:button id="ruleview-add-rule-button" title="&addRuleButtonTooltip;" class="devtools-button"></html:button>
-                <html:button id="pseudo-class-panel-toggle" title="&togglePseudoClassPanel;" class="devtools-button"></html:button>
-              </html:div>
-            </html:div>
-            <html:div id="pseudo-class-panel" hidden="true">
-              <html:label><html:input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</html:label>
-              <html:label><html:input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</html:label>
-              <html:label><html:input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</html:label>
-          </html:div>
-          </html:div>
+    <vbox id="inspector-sidebar-container">
+      <!-- Specify the XHTML namespace explicitly
+        otherwise the layout is broken. -->
+      <div xmlns="http://www.w3.org/1999/xhtml"
+           id="inspector-sidebar"
+           hidden="true" />
+    </vbox>
 
-          <html:div id="ruleview-container" class="ruleview">
-          </html:div>
-        </tabpanel>
-
-        <tabpanel id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div class="devtools-toolbar">
+    <!-- Sidebar panel definitions -->
+    <html:div xmlns="http://www.w3.org/1999/xhtml" id="tabpanels" style="visibility:collapse">
+      <html:div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div id="ruleview-toolbar-container" class="devtools-toolbar">
+          <html:div id="ruleview-toolbar">
             <html:div class="devtools-searchbox">
-              <html:input id="computedview-searchbox"
+              <html:input id="ruleview-searchbox"
                           class="devtools-filterinput devtools-rule-searchbox"
                           type="search"
                           placeholder="&filterStylesPlaceholder;"/>
-              <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+              <html:button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+            </html:div>
+            <html:div id="ruleview-command-toolbar">
+              <html:button id="ruleview-add-rule-button" title="&addRuleButtonTooltip;" class="devtools-button"></html:button>
+              <html:button id="pseudo-class-panel-toggle" title="&togglePseudoClassPanel;" class="devtools-button"></html:button>
             </html:div>
-            <checkbox id="browser-style-checkbox"
-                      class="includebrowserstyles"
-                      checked="false"
-                      label="&browserStylesLabel;"/>
           </html:div>
+          <html:div id="pseudo-class-panel" hidden="true">
+            <html:label><html:input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</html:label>
+            <html:label><html:input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</html:label>
+            <html:label><html:input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</html:label>
+        </html:div>
+        </html:div>
 
-          <html:div id="propertyContainer">
-          </html:div>
-
-          <html:div id="computedview-no-results" hidden="">
-            &noPropertiesFound;
-          </html:div>
-        </tabpanel>
+        <html:div id="ruleview-container" class="ruleview">
+        </html:div>
+      </html:div>
 
-        <tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div id="layout-wrapper">
-            <html:div id="layout-container">
-              <html:p id="layout-header">
-                <html:span id="layout-element-size"></html:span>
-                <html:section id="layout-position-group">
-                  <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
-                  <html:span id="layout-element-position"></html:span>
-                </html:section>
-              </html:p>
+      <html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div class="devtools-toolbar">
+          <html:div class="devtools-searchbox">
+            <html:input id="computedview-searchbox"
+                        class="devtools-filterinput devtools-rule-searchbox"
+                        type="search"
+                        placeholder="&filterStylesPlaceholder;"/>
+            <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+          </html:div>
+          <html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
+            <html:input id="browser-style-checkbox"
+                        type="checkbox"
+                        class="includebrowserstyles"
+                        label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
+        </html:div>
+
+        <html:div id="propertyContainer">
+        </html:div>
 
-              <html:div id="layout-main">
-                <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
-                <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
-                  <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
-                  <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
-                    <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
-                    <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
-                      <html:div id="layout-content" data-box="content" title="&content.tooltip;">
-                      </html:div>
+        <html:div id="computedview-no-results" hidden="">
+          &noPropertiesFound;
+        </html:div>
+      </html:div>
+
+      <html:div id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div id="layout-wrapper">
+          <html:div id="layout-container">
+            <html:p id="layout-header">
+              <html:span id="layout-element-size"></html:span>
+              <html:section id="layout-position-group">
+                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+                <html:span id="layout-element-position"></html:span>
+              </html:section>
+            </html:p>
+
+            <html:div id="layout-main">
+              <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
+              <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
+                <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
+                <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
+                  <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
+                  <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
+                    <html:div id="layout-content" data-box="content" title="&content.tooltip;">
                     </html:div>
                   </html:div>
                 </html:div>
-
-                <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
-                <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
-                <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
-                <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
-
-                <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
-                <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
-                <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
-                <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
-
-                <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
-                <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
-                <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
-                <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
-
-                <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
               </html:div>
 
-              <html:div style="display: none">
-                <html:p id="layout-dummy"></html:p>
-              </html:div>
+              <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
+              <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
+              <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
+              <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
+
+              <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
+              <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
+              <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
+              <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
+
+              <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
+              <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
+              <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
+              <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
+
+              <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
+            </html:div>
+
+            <html:div style="display: none">
+              <html:p id="layout-dummy"></html:p>
             </html:div>
           </html:div>
-        </tabpanel>
+        </html:div>
+      </html:div>
 
-        <tabpanel id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div class="devtools-toolbar">
-            <html:div class="devtools-searchbox">
-              <html:input id="font-preview-text-input"
-                          class="devtools-textinput"
-                          type="search"
-                          placeholder="&previewHint;"/>
-            </html:div>
+      <html:div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div class="devtools-toolbar">
+          <html:div class="devtools-searchbox">
+            <html:input id="font-preview-text-input"
+                        class="devtools-textinput"
+                        type="search"
+                        placeholder="&previewHint;"/>
           </html:div>
+        </html:div>
 
-          <html:div id="font-container">
-            <html:ul id="all-fonts"></html:ul>
-            <html:button id="font-showall">&showAllFonts;</html:button>
-          </html:div>
+        <html:div id="font-container">
+          <html:ul id="all-fonts"></html:ul>
+          <html:button id="font-showall">&showAllFonts;</html:button>
+        </html:div>
 
-          <html:div id="font-template">
-            <html:section class="font">
-              <html:div class="font-preview-container">
-                <html:img class="font-preview"></html:img>
-              </html:div>
-              <html:div class="font-info">
-                <html:h1 class="font-name"></html:h1>
-                <html:span class="font-is-local">&system;</html:span>
-                <html:span class="font-is-remote">&remote;</html:span>
-                <html:p class="font-format-url">
-                  <html:input readonly="readonly" class="font-url"></html:input>
-                  <html:span class="font-format"></html:span>
-                </html:p>
-                <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
-                <html:pre class="font-css-code"></html:pre>
-              </html:div>
-            </html:section>
-          </html:div>
-        </tabpanel>
-      </tabpanels>
-    </tabbox>
+        <html:div id="font-template">
+          <html:section class="font">
+            <html:div class="font-preview-container">
+              <html:img class="font-preview"></html:img>
+            </html:div>
+            <html:div class="font-info">
+              <html:h1 class="font-name"></html:h1>
+              <html:span class="font-is-local">&system;</html:span>
+              <html:span class="font-is-remote">&remote;</html:span>
+              <html:p class="font-format-url">
+                <html:input readonly="readonly" class="font-url"></html:input>
+                <html:span class="font-format"></html:span>
+              </html:p>
+              <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
+              <html:pre class="font-css-code"></html:pre>
+            </html:div>
+          </html:section>
+        </html:div>
+      </html:div>
+
+      <html:div id="sidebar-panel-animationinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:iframe class="devtools-inspector-tab-frame" />
+      </html:div>
+    </html:div>
+
   </box>
 </window>
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,21 +1,23 @@
 # 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/.
 
 DIRS += [
+    'components',
     'computed',
     'fonts',
     'layout',
     'markup',
     'rules',
     'shared'
 ]
 
 DevToolsModules(
     'breadcrumbs.js',
     'inspector-commands.js',
     'inspector-panel.js',
-    'inspector-search.js'
+    'inspector-search.js',
+    'toolsidebar.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
@@ -6,17 +6,17 @@
 // Test that the inspector toggled panel is visible by default, is hidden after
 // clicking on the toggle button and remains expanded/collapsed when switching
 // hosts.
 
 add_task(function* () {
   info("Open the inspector in a side toolbox host");
   let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side");
 
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
   ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
   EventUtils.synthesizeMouseAtCenter(button, {},
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
@@ -5,17 +5,17 @@
 
 // Test that the toggle button can collapse and expand the inspector side/bottom
 // panel, and that the appropriate attributes are updated in the process.
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL("about:blank");
 
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
 
   ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
   EventUtils.synthesizeMouseAtCenter(button, {},
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
@@ -15,17 +15,17 @@ add_task(function* () {
     let options = {"set": [
       ["devtools.toolsidebar-width.inspector", 200]
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { inspector, toolbox } = yield openInspectorForURL("about:blank");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
 
   info("Changing toolbox host to a window.");
   yield toolbox.switchHost(Toolbox.HostType.WINDOW);
 
   let hostWindow = toolbox._host._window;
   let originalWidth = hostWindow.outerWidth;
   let originalHeight = hostWindow.outerHeight;
 
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
@@ -5,17 +5,17 @@
 
 /**
 * Test the keyboard navigation for the pane toggle using
 * space and enter
 */
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL("about:blank", "side");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
 
   ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   yield togglePane(button, "Press on the toggle button", panel, "VK_RETURN");
   ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
 
   yield togglePane(button, "Press on the toggle button to expand the panel again",
--- a/devtools/client/inspector/test/browser_inspector_sidebarstate.js
+++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
@@ -13,16 +13,20 @@ add_task(function* () {
   inspector.sidebar.select("ruleview");
 
   is(inspector.sidebar.getCurrentTabID(), "ruleview",
      "Rule View is selected by default");
 
   info("Selecting computed view.");
   inspector.sidebar.select("computedview");
 
+  // Finish initialization of the computed panel before
+  // destroying the toolbox.
+  yield waitForTick();
+
   info("Closing inspector.");
   yield toolbox.destroy();
 
   info("Re-opening inspector.");
   inspector = (yield openInspector()).inspector;
 
   if (!inspector.sidebar.getCurrentTabID()) {
     info("Default sidebar still to be selected, adding select listener.");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/toolsidebar.js
@@ -0,0 +1,320 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Services = require("Services");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var { Task } = require("devtools/shared/task");
+var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * This object represents replacement for ToolSidebar
+ * implemented in devtools/client/framework/sidebar.js module
+ *
+ * This new component is part of devtools.html aimed at
+ * removing XUL and use HTML for entire DevTools UI.
+ * There are currently two implementation of the side bar since
+ * the `sidebar.js` module (mentioned above) is still used by
+ * other panels.
+ * As soon as all panels are using this HTML based
+ * implementation it can be removed.
+ */
+function ToolSidebar(tabbox, panel, uid, options = {}) {
+  EventEmitter.decorate(this);
+
+  this._tabbox = tabbox;
+  this._uid = uid;
+  this._panelDoc = this._tabbox.ownerDocument;
+  this._toolPanel = panel;
+  this._options = options;
+
+  if (!options.disableTelemetry) {
+    this._telemetry = new Telemetry();
+  }
+
+  this._tabs = [];
+
+  if (this._options.hideTabstripe) {
+    this._tabbox.setAttribute("hidetabs", "true");
+  }
+
+  this.render();
+
+  this._toolPanel.emit("sidebar-created", this);
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+  TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+  // React
+
+  get React() {
+    return this._toolPanel.React;
+  },
+
+  get ReactDOM() {
+    return this._toolPanel.ReactDOM;
+  },
+
+  get browserRequire() {
+    return this._toolPanel.browserRequire;
+  },
+
+  get InspectorTabPanel() {
+    if (!this._InspectorTabPanel) {
+      this._InspectorTabPanel =
+        this.React.createFactory(this.browserRequire(
+        "devtools/client/inspector/components/inspector-tab-panel"));
+    }
+    return this._InspectorTabPanel;
+  },
+
+  // Rendering
+
+  render: function () {
+    let Tabbar = this.React.createFactory(this.browserRequire(
+      "devtools/client/shared/components/tabs/tabbar"));
+
+    let sidebar = Tabbar({
+      onSelect: this.handleSelectionChange.bind(this),
+    });
+
+    this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
+  },
+
+  addExistingTab: function (id, title, selected) {
+    this._tabbar.addTab(id, title, selected, this.InspectorTabPanel);
+
+    this.emit("new-tab-registered", id);
+  },
+
+  /**
+   * Register a tab. A tab is a document.
+   * The document must have a title, which will be used as the name of the tab.
+   *
+   * @param {string} tab uniq id
+   * @param {string} url
+   */
+  addFrameTab: function (id, title, url, selected) {
+    let panel = this.InspectorTabPanel({
+      id: id,
+      key: id,
+      title: title,
+      url: url,
+      onMount: this.onSidePanelMounted.bind(this),
+    });
+
+    this._tabbar.addTab(id, title, selected, panel);
+
+    this.emit("new-tab-registered", id);
+  },
+
+  onSidePanelMounted: function (content, props) {
+    let iframe = content.querySelector("iframe");
+    if (!iframe || iframe.getAttribute("src")) {
+      return;
+    }
+
+    let onIFrameLoaded = (event) => {
+      iframe.removeEventListener("load", onIFrameLoaded, true);
+
+      let doc = event.target;
+      let win = doc.defaultView;
+      if ("setPanel" in win) {
+        win.setPanel(this._toolPanel, iframe);
+      }
+      this.emit(props.id + "-ready");
+    };
+
+    iframe.addEventListener("load", onIFrameLoaded, true);
+    iframe.setAttribute("src", props.url);
+  },
+
+  /**
+   * Remove an existing tab.
+   * @param {String} tabId The ID of the tab that was used to register it, or
+   * the tab id attribute value if the tab existed before the sidebar
+   * got created.
+   * @param {String} tabPanelId Optional. If provided, this ID will be used
+   * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+   */
+  removeTab: Task.async(function* (tabId, tabPanelId) {
+    this._tabbar.removeTab(tabId);
+
+    let win = this.getWindowForTab(tabId);
+    if (win && ("destroy" in win)) {
+      yield win.destroy();
+    }
+
+    this.emit("tab-unregistered", tabId);
+  }),
+
+  /**
+   * Show or hide a specific tab.
+   * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
+   * @param {String} id The ID of the tab to be hidden.
+   */
+  toggleTab: function (isVisible, id) {
+    this._tabbar.toggleTab(id, isVisible);
+  },
+
+  /**
+   * Select a specific tab.
+   */
+  select: function (id) {
+    this._tabbar.select(id);
+  },
+
+  /**
+   * Return the id of the selected tab.
+   */
+  getCurrentTabID: function () {
+    return this._currentTool;
+  },
+
+  /**
+   * Returns the requested tab panel based on the id.
+   * @param {String} id
+   * @return {DOMNode}
+   */
+  getTabPanel: function (id) {
+    // Search with and without the ID prefix as there might have been existing
+    // tabpanels by the time the sidebar got created
+    return this._panelDoc.querySelector("#" +
+      this.TABPANEL_ID_PREFIX + id + ", #" + id);
+  },
+
+  /**
+   * Event handler.
+   */
+  handleSelectionChange: function (id) {
+    if (this._destroyed) {
+      return;
+    }
+
+    let previousTool = this._currentTool;
+    if (previousTool) {
+      if (this._telemetry) {
+        this._telemetry.toolClosed(previousTool);
+      }
+      this.emit(previousTool + "-unselected");
+    }
+
+    this._currentTool = id;
+
+    if (this._telemetry) {
+      this._telemetry.toolOpened(this._currentTool);
+    }
+
+    this.emit(this._currentTool + "-selected");
+    this.emit("select", this._currentTool);
+  },
+
+  /**
+   * Show the sidebar.
+   *
+   * @param  {String} id
+   *         The sidebar tab id to select.
+   */
+  show: function (id) {
+    this._tabbox.removeAttribute("hidden");
+
+    // If an id is given, select the corresponding sidebar tab and record the
+    // tool opened.
+    if (id) {
+      this._currentTool = id;
+
+      if (this._telemetry) {
+        this._telemetry.toolOpened(this._currentTool);
+      }
+    }
+
+    this.emit("show");
+  },
+
+  /**
+   * Show the sidebar.
+   */
+  hide: function () {
+    this._tabbox.setAttribute("hidden", "true");
+
+    this.emit("hide");
+  },
+
+  /**
+   * Return the window containing the tab content.
+   */
+  getWindowForTab: function (id) {
+    // Get the tabpanel and make sure it contains an iframe
+    let panel = this.getTabPanel(id);
+    if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
+      return null;
+    }
+
+    return panel.firstElementChild.contentWindow;
+  },
+
+  /**
+   * Clean-up.
+   */
+  destroy: Task.async(function* () {
+    if (this._destroyed) {
+      return;
+    }
+    this._destroyed = true;
+
+    this.emit("destroy");
+
+    // Note that we check for the existence of this._tabbox.tabpanels at each
+    // step as the container window may have been closed by the time one of the
+    // panel's destroy promise resolves.
+    let tabpanels = [...this._tabbox.querySelectorAll(".tab-panel-box")];
+    for (let panel of tabpanels) {
+      let iframe = panel.querySelector("iframe");
+      if (!iframe) {
+        continue;
+      }
+      let win = iframe.contentWindow;
+      if (win && ("destroy" in win)) {
+        yield win.destroy();
+      }
+      panel.remove();
+    }
+
+    if (this._currentTool && this._telemetry) {
+      this._telemetry.toolClosed(this._currentTool);
+    }
+
+    this._toolPanel.emit("sidebar-destroyed", this);
+
+    this._tabs = null;
+    this._tabbox = null;
+    this._panelDoc = null;
+    this._toolPanel = null;
+  })
+};
+
+XPCOMUtils.defineLazyGetter(this, "l10n", function () {
+  let bundle = Services.strings.createBundle(
+    "chrome://devtools/locale/toolbox.properties");
+
+  let l10n = function (name, ...args) {
+    try {
+      if (args.length == 0) {
+        return bundle.GetStringFromName(name);
+      }
+      return bundle.formatStringFromName(name, args, args.length);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  };
+  return l10n;
+});
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -37,15 +37,21 @@
 
 /******************************************************************************/
 /* Theme Firebug */
 
 .theme-firebug .panelContent {
   height: calc(100% - 30px);
 }
 
+/* JSON View is using bigger font-size for the main tabs so,
+  let's overwrite the default value. */
+.theme-firebug .tabs .tabs-navigation {
+  font-size: 14px;
+}
+
 /******************************************************************************/
 /* Theme Light & Theme Dark*/
 
 .theme-dark .panelContent,
 .theme-light .panelContent {
   height: calc(100% - 27px);
 }
--- a/devtools/client/jsonview/json-viewer.js
+++ b/devtools/client/jsonview/json-viewer.js
@@ -24,17 +24,17 @@ define(function (require, exports, modul
   }
 
   // Application state object.
   let input = {
     jsonText: json.textContent,
     jsonPretty: null,
     json: jsonData,
     headers: JSON.parse(headers.textContent),
-    tabActive: 1,
+    tabActive: 0,
     prettified: false
   };
 
   json.remove();
   headers.remove();
 
   /**
    * Application actions/commands. This list implements all commands
--- a/devtools/client/locales/en-US/font-inspector.dtd
+++ b/devtools/client/locales/en-US/font-inspector.dtd
@@ -1,16 +1,15 @@
 <!-- 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/. -->
 
 <!-- LOCALIZATION NOTE : FILE This file contains the Font Inspector strings.
   - The Font Inspector is the panel accessible in the Inspector sidebar. -->
 
-<!ENTITY fontInspectorTitle "Fonts">
 <!ENTITY showAllFonts "See all the fonts used in the page">
 <!ENTITY usedAs "Used as: ">
 <!ENTITY system "system">
 <!ENTITY remote "remote">
 
 <!-- LOCALIZATION NOTE (previewHint): This is the label shown as the
      placeholder in font inspector preview text box. -->
 <!ENTITY previewHint "Preview Text">
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -327,8 +327,35 @@ markupView.hide.key=h
 # LOCALIZATION NOTE (markupView.edit.key):
 # Key shortcut used to hide the selected node in the markup view.
 markupView.edit.key=F2
 
 # LOCALIZATION NOTE (markupView.scrollInto.key):
 # Key shortcut used to scroll the webpage in order to ensure the selected node
 # is visible
 markupView.scrollInto.key=s
+
+# LOCALIZATION NOTE (inspector.sidebar.fontInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of fonts used in the page.
+inspector.sidebar.fontInspectorTitle=Fonts
+
+# LOCALIZATION NOTE (inspector.sidebar.ruleViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of CSS rules used
+# in the page.
+inspector.sidebar.ruleViewTitle=Rules
+
+# LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of computed CSS values
+# used in the page.
+inspector.sidebar.computedViewTitle=Computed
+
+# LOCALIZATION NOTE (inspector.sidebar.layoutViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying box model of the selected element.
+inspector.sidebar.layoutViewTitle=Box Model
+
+# LOCALIZATION NOTE (inspector.sidebar.animationInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying animations defined in the page.
+inspector.sidebar.animationInspectorTitle=Animations
--- a/devtools/client/locales/en-US/layoutview.dtd
+++ b/devtools/client/locales/en-US/layoutview.dtd
@@ -11,17 +11,16 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (*.tooltip): These tooltips are not regular tooltips.
   -  The text appears on the bottom right corner of the layout view when
   -  the corresponding box is hovered. -->
 
-<!ENTITY layoutViewTitle          "Box Model">
 <!ENTITY margin.tooltip           "margin">
 <!ENTITY border.tooltip           "border">
 <!ENTITY padding.tooltip          "padding">
 <!ENTITY content.tooltip          "content">
 
 <!-- LOCALIZATION NOTE: This label is displayed as a tooltip that appears when
   -  hovering over the button that allows users to edit the position of an
   -  element in the page. -->
--- a/devtools/client/locales/en-US/styleinspector.dtd
+++ b/devtools/client/locales/en-US/styleinspector.dtd
@@ -31,12 +31,8 @@
   -  shown when hovering over the `Toggle Pseudo Class Panel` button in the
   -  rule view toolbar. -->
 <!ENTITY togglePseudoClassPanel  "Toggle pseudo-classes">
 
 <!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS
   -  properties to display e.g. due to search criteria this message is
   -  displayed. -->
 <!ENTITY noPropertiesFound     "No CSS properties found.">
-
-<!-- FIXME: notes -->
-<!ENTITY computedViewTitle     "Computed">
-<!ENTITY ruleViewTitle         "Rules">
--- a/devtools/client/responsivedesign/test/browser.ini
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -13,8 +13,9 @@ skip-if = e10s && debug # Bug 1252201 - 
 [browser_responsiveruleview.js]
 skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
 [browser_responsiveui.js]
 [browser_responsiveui_touch.js]
 [browser_responsiveuiaddcustompreset.js]
 [browser_responsive_devicewidth.js]
 [browser_responsiveui_customuseragent.js]
 [browser_responsiveui_window_close.js]
+skip-if = (os == 'linux') && e10s && debug # Bug 1277274
--- a/devtools/client/shared/components/reps/text-node.js
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -6,19 +6,18 @@
 "use strict";
 
 // Make this available to both AMD and CJS environments
 define(function (require, exports, module) {
   // ReactJS
   const React = require("devtools/client/shared/vendor/react");
 
   // Reps
-  const { createFactories, isGrip } = require("./rep-utils");
+  const { createFactories, isGrip, cropMultipleLines } = require("./rep-utils");
   const { ObjectBox } = createFactories(require("./object-box"));
-  const { cropMultipleLines } = require("./string");
 
   // Shortcuts
   const DOM = React.DOM;
 
   /**
    * Renders DOM #text node.
    */
   let TextNode = React.createClass({
--- a/devtools/client/shared/components/tabs/moz.build
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -1,10 +1,12 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'tabbar.css',
+    'tabbar.js',
     'tabs.css',
     'tabs.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,54 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.tabs .tabs-navigation {
+  line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+  height: 23px;
+}
+
+.tabs .tabs-menu-item:first-child {
+  border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+  outline: var(--theme-focus-outline);
+  outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+  height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+  height: 24px;
+}
+
+.tabs .tabs-menu-item a {
+  cursor: default;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+  should stretch accordingly. Use flexbox for the behavior. */
+.tabs .tabs-navigation .tabs-menu {
+  display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+  flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+  text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu {
+  display: block;
+}
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,160 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+  displayName: "Tabbar",
+
+  propTypes: {
+    onSelect: PropTypes.func,
+  },
+
+  getInitialState: function () {
+    return {
+      tabs: [],
+      activeTab: 0
+    };
+  },
+
+  // Public API
+
+  addTab: function (id, title, selected = false, panel, url) {
+    let tabs = this.state.tabs.slice();
+    tabs.push({id, title, panel, url});
+
+    let newState = Object.assign({}, this.state, {
+      tabs: tabs,
+    });
+
+    if (selected) {
+      newState.activeTab = tabs.length - 1;
+    }
+
+    this.setState(newState);
+  },
+
+  toggleTab: function (tabId, isVisible) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let tabs = this.state.tabs.slice();
+    tabs[index] = Object.assign({}, tabs[index], {
+      isVisible: isVisible
+    });
+
+    this.setState(Object.assign({}, this.state, {
+      tabs: tabs,
+    }));
+  },
+
+  removeTab: function (tabId) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let tabs = this.state.tabs.slice();
+    tabs.splice(index, 1);
+
+    this.setState(Object.assign({}, this.state, {
+      tabs: tabs,
+    }));
+  },
+
+  select: function (tabId) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let newState = Object.assign({}, this.state, {
+      activeTab: index,
+    });
+
+    this.setState(newState, () => {
+      if (this.props.onSelect) {
+        this.props.onSelect(tabId);
+      }
+    });
+  },
+
+  // Helpers
+
+  getTabIndex: function (tabId) {
+    let tabIndex = -1;
+    this.state.tabs.forEach((tab, index) => {
+      if (tab.id == tabId) {
+        tabIndex = index;
+      }
+    });
+    return tabIndex;
+  },
+
+  getTabId: function (index) {
+    return this.state.tabs[index].id;
+  },
+
+  getCurrentTabId: function () {
+    return this.state.tabs[this.state.activeTab].id;
+  },
+
+  // Event Handlers
+
+  onTabChanged: function (index) {
+    this.setState({
+      activeTab: index
+    });
+
+    if (this.props.onSelect) {
+      this.props.onSelect(this.state.tabs[index].id);
+    }
+  },
+
+  // Rendering
+
+  renderTab: function (tab) {
+    if (typeof tab.panel === "function") {
+      return tab.panel({
+        key: tab.id,
+        title: tab.title,
+        id: tab.id,
+        url: tab.url,
+      });
+    }
+
+    return tab.panel;
+  },
+
+  render: function () {
+    let tabs = this.state.tabs.map(tab => {
+      return this.renderTab(tab);
+    });
+
+    return (
+      div({className: "devtools-sidebar-tabs"},
+        Tabs({
+          tabActive: this.state.activeTab,
+          onAfterChange: this.onTabChanged},
+          tabs
+        )
+      )
+    );
+  },
+});
+
+module.exports = Tabbar;
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -1,124 +1,65 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-/******************************************************************************/
 /* Tabs General Styles */
 
 .tabs {
   height: 100%;
 }
 
-.tabs .tabs-navigation {
-  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
-  font-size: 14px;
-  border-bottom-color: rgb(170, 188, 207);
-}
-
 .tabs .tabs-menu {
   display: table;
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
 .tabs .tabs-menu-item {
   float: left;
 }
 
 .tabs .tabs-menu-item a {
   display: block;
   color: #A9A9A9;
   padding: 4px 8px;
-}
-
-.tabs .tabs-menu-item a {
   border: 1px solid transparent;
   text-decoration: none;
+  white-space: nowrap;
 }
 
-.tabs .tab-panel {
-  background-color: white;
+/* Make sure panel content takes entire vertical space.
+  (minus the height of the tab bar) */
+.tabs .panels {
+  height: calc(100% - 24px);
 }
 
-.tabs .tab-panel > DIV,
-.tabs .tab-panel > DIV > DIV {
+.tabs .tab-panel-box,
+.tabs .tab-panel {
   height: 100%;
 }
 
-/******************************************************************************/
-/* Firebug Theme */
-
-.theme-firebug .tabs {
-  background-color: rgb(219, 234, 249);
-  background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
-}
-
-.theme-firebug .tabs .tabs-navigation {
-  padding-top: 3px;
-  padding-left: 3px;
-  height: 27px;
-  border-bottom: 1px solid rgb(170, 188, 207);
-}
-
-.theme-firebug .tabs .tab-panel {
-  height: calc(100% - 31px); /* minus the height of the tab bar */
-}
-
-.theme-firebug .tabs .tabs-menu-item {
-  position: relative;
-}
-
-.theme-firebug .tabs .tabs-menu-item a {
-  padding: 5px 8px 4px 8px;;
-  font-weight: bold;
-  color: #565656;
-  border-radius: 4px 4px 0 0;
-}
-
-.theme-firebug .tabs .tabs-menu-item.is-active a {
-  background-color: rgb(247, 251, 254);
-  border: 1px solid rgb(170, 188, 207);
-  border-bottom-color: transparent;
-}
-
-.theme-firebug .tabs .tabs-menu-item:hover a {
-  border: 1px solid #C8C8C8;
-  border-bottom: 1px solid transparent;
-  background-color: transparent !important;
-}
-
-.theme-firebug .tabs .tabs-menu-item.is-active:hover a {
-  border: 1px solid rgb(170, 188, 207) !important;
-  background-color: rgb(247, 251, 254) !important;
-  border-bottom-color: transparent !important;
-}
-
-/******************************************************************************/
 /* Light Theme */
 
 .theme-dark .tabs,
 .theme-light .tabs {
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-navigation,
 .theme-light .tabs .tabs-navigation {
   border-bottom: 1px solid var(--theme-splitter-color);
-  box-shadow: 0 -2px 0 rgba(170, 170, 170,.1) inset;
-  height: 24px;
   font-size: 12px;
 }
 
-.theme-dark .tabs .tab-panel,
-.theme-light .tabs .tab-panel {
-  height: calc(100% - 24px); /* minus the height of the tab bar */
+.theme-firebug .tabs .tabs-navigation {
+  font-size: 11px;
 }
 
 .theme-dark .tabs .tabs-menu-item,
 .theme-light .tabs .tabs-menu-item {
   margin: 0;
   padding: 0;
   border-style: solid;
   border-width: 0;
@@ -130,19 +71,17 @@
 .theme-light .tabs .tabs-menu-item a {
   color: var(--theme-content-color1);
 }
 
 .theme-dark .tabs .tabs-menu-item a:hover,
 .theme-dark .tabs .tabs-menu-item a,
 .theme-light .tabs .tabs-menu-item a:hover,
 .theme-light .tabs .tabs-menu-item a {
-  border: none !important;
-  background-color: transparent !important;
-  padding: 5px 15px;
+  padding: 3px 15px;
 }
 
 .theme-dark .tabs .tabs-menu-item:hover,
 .theme-light .tabs .tabs-menu-item:hover {
   background-color: var(--toolbar-tab-hover);
 }
 
 .theme-dark .tabs .tabs-menu-item.is-active,
@@ -154,51 +93,72 @@
 
 .theme-dark .tabs .tabs-menu-item.is-active a,
 .theme-dark .tabs .tabs-menu-item.is-active:hover a,
 .theme-light .tabs .tabs-menu-item.is-active a,
 .theme-light .tabs .tabs-menu-item.is-active:hover a {
   color: var(--theme-selection-color);
 }
 
-.theme-dark .tabs .tabs-menu-item:active:hover,
-.theme-light .tabs .tabs-menu-item:active:hover {
-  background-color: var(--toolbar-tab-hover-active);
-}
-
-.theme-dark .tabs .tabs-menu-item.is-active,
-.theme-light .tabs .tabs-menu-item.is-active {
-  box-shadow: 0 2px 0 #d7f1ff inset,
-              0 8px 3px -5px #2b82bf inset,
-              0 -2px 0 rgba(0,0,0,.06) inset;
-}
-
-/******************************************************************************/
 /* Dark Theme */
 
-.theme-dark .tabs .tabs-navigation {
-  box-shadow: 0px -2px 0px rgba(0, 0, 0, 0.1) inset;
-}
-
 .theme-dark .tabs .tabs-menu-item a {
   color: #CED3D9;
 }
 
-.theme-dark .tabs .tabs-menu-item a:hover,
-.theme-dark .tabs .tabs-menu-item a {
-  border: none !important;
-  background-color: transparent !important;
-  padding: 5px 15px;
-}
-
 .theme-dark .tabs .tabs-menu-item:active:hover {
   background-color: hsla(206, 37%, 4%, .4); /* --toolbar-tab-hover-active */
 }
 
-.theme-dark .tabs .tabs-menu-item.is-active {
-  box-shadow: 0px 2px 0px #D7F1FF inset,
-   0px 8px 3px -5px #2B82BF inset,
-   0px -2px 0px rgba(0, 0, 0, 0.2) inset;
+/* Firebug Theme */
+
+.theme-firebug .tabs {
+  background-color: rgb(219, 234, 249);
+  background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+}
+
+.theme-firebug .tabs .tabs-navigation {
+  padding-top: 3px;
+  padding-left: 3px;
+  height: 27px;
+  border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+  background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+  position: relative;
+  border-inline-start-width: 0;
 }
 
-.theme-dark .tabs .tab-panel {
-  background-color: var(--theme-body-background);
+.theme-firebug .tabs .tabs-menu-item a {
+  font-family: var(--proportional-font-family);
+  font-weight: bold;
+  color: var(--theme-body-color);
+  border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover a {
+  border: 1px solid #C8C8C8;
+  border-bottom: 1px solid transparent;
+  background-color: transparent;
 }
+
+.theme-firebug .tabs .tabs-menu-item.is-active a,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover a {
+  background-color: rgb(247, 251, 254);
+  border: 1px solid rgb(170, 188, 207);
+  border-bottom-color: transparent;
+  color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a:hover,
+.theme-firebug .tabs .tabs-menu-item a {
+  border: 1px solid transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item a:hover,
+.theme-firebug .tabs .tabs-menu-item a {
+  padding: 4px 8px 4px 8px;
+}
--- a/devtools/client/shared/components/tabs/tabs.js
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -3,36 +3,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";
 
 define(function (require, exports, module) {
   const React = require("devtools/client/shared/vendor/react");
-  const DOM = React.DOM;
+  const { DOM } = React;
+  const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 
   /**
    * Renders simple 'tab' widget.
    *
    * Based on ReactSimpleTabs component
    * https://github.com/pedronauck/react-simpletabs
    *
    * Component markup (+CSS) example:
    *
    * <div class='tabs'>
    *  <nav class='tabs-navigation'>
    *    <ul class='tabs-menu'>
    *      <li class='tabs-menu-item is-active'>Tab #1</li>
    *      <li class='tabs-menu-item'>Tab #2</li>
    *    </ul>
    *  </nav>
-   *  <article class='tab-panel'>
+   *  <div class='tab-panel'>
    *    The content of active panel here
-   *  </article>
+   *  </div>
    * <div>
    */
   let Tabs = React.createClass({
     displayName: "Tabs",
 
     propTypes: {
       className: React.PropTypes.oneOfType([
         React.PropTypes.array,
@@ -46,124 +47,236 @@ define(function (require, exports, modul
       children: React.PropTypes.oneOfType([
         React.PropTypes.array,
         React.PropTypes.element
       ]).isRequired
     },
 
     getDefaultProps: function () {
       return {
-        tabActive: 1
+        tabActive: 0
       };
     },
 
     getInitialState: function () {
       return {
-        tabActive: this.props.tabActive
+        tabActive: this.props.tabActive,
+
+        // This array is used to store an information whether a tab
+        // at specific index has already been created (e.g. selected
+        // at least once).
+        // If yes, it's rendered even if not currently selected.
+        // This is because in some cases we don't want to re-create
+        // tab content when it's being unselected/selected.
+        // E.g. in case of an iframe being used as a tab-content
+        // we want the iframe to stay in the DOM.
+        created: [],
       };
     },
 
     componentDidMount: function () {
+      let node = findDOMNode(this);
+      node.addEventListener("keydown", this.onKeyDown, false);
+
       let index = this.state.tabActive;
       if (this.props.onMount) {
         this.props.onMount(index);
       }
     },
 
     componentWillReceiveProps: function (newProps) {
       if (newProps.tabActive) {
-        this.setState({tabActive: newProps.tabActive});
+        let created = [...this.state.created];
+        created[newProps.tabActive] = true;
+
+        this.setState(Object.assign({}, this.state, {
+          tabActive: newProps.tabActive,
+          created: created,
+        }));
       }
     },
 
-    setActive: function (index, e) {
+    componentWillUnmount: function () {
+      let node = findDOMNode(this);
+      node.removeEventListener("keydown", this.onKeyDown, false);
+    },
+
+    // DOM Events
+
+    onKeyDown: function (event) {
+      // Bail out if the focus isn't on a tab.
+      if (!event.target.closest(".tabs-menu-item")) {
+        return;
+      }
+
+      let tabActive = this.state.tabActive;
+      let tabCount = this.props.children.length;
+
+      switch (event.code) {
+        case "ArrowRight":
+          tabActive = Math.min(tabCount - 1, tabActive + 1);
+          break;
+        case "ArrowLeft":
+          tabActive = Math.max(0, tabActive - 1);
+          break;
+      }
+
+      if (this.state.tabActive != tabActive) {
+        this.setActive(tabActive);
+      }
+    },
+
+    onClickTab: function (index, event) {
+      this.setActive(index);
+      event.preventDefault();
+    },
+
+    // API
+
+    setActive: function (index) {
       let onAfterChange = this.props.onAfterChange;
       let onBeforeChange = this.props.onBeforeChange;
 
       if (onBeforeChange) {
         let cancel = onBeforeChange(index);
         if (cancel) {
           return;
         }
       }
 
-      let newState = {
-        tabActive: index
-      };
+      let created = [...this.state.created];
+      created[index] = true;
+
+      let newState = Object.assign({}, this.state, {
+        tabActive: index,
+        created: created
+      });
 
       this.setState(newState, () => {
+        // Properly set focus on selected tab.
+        let node = findDOMNode(this);
+        let selectedTab = node.querySelector(".is-active > a");
+        if (selectedTab) {
+          selectedTab.focus();
+        }
+
         if (onAfterChange) {
           onAfterChange(index);
         }
       });
-
-      e.preventDefault();
     },
 
-    getMenuItems: function () {
+    // Rendering
+
+    renderMenuItems: function () {
       if (!this.props.children) {
-        throw new Error("Tabs must contain at least one Panel");
+        throw new Error("There must be at least one Tab");
       }
 
       if (!Array.isArray(this.props.children)) {
         this.props.children = [this.props.children];
       }
 
-      let menuItems = this.props.children
-        .map(function (panel) {
-          return typeof panel === "function" ? panel() : panel;
-        }).filter(function (panel) {
-          return panel;
-        }).map(function (panel, index) {
-          let ref = ("tab-menu-" + (index + 1));
-          let title = panel.props.title;
-          let tabClassName = panel.props.className;
+      let tabs = this.props.children
+        .map(tab => {
+          return typeof tab === "function" ? tab() : tab;
+        }).filter(tab => {
+          return tab;
+        }).map((tab, index) => {
+          let ref = ("tab-menu-" + index);
+          let title = tab.props.title;
+          let tabClassName = tab.props.className;
 
           let classes = [
             "tabs-menu-item",
             tabClassName,
-            this.state.tabActive === (index + 1) && "is-active"
+            this.state.tabActive === index ? "is-active" : ""
           ].join(" ");
 
+          // Set tabindex to -1 (except the selected tab) so, it's focusable,
+          // but not reachable via sequential tab-key navigation.
+          // Changing selected tab (and so, moving focus) is done through
+          // left and right arrow keys.
+          // See also `onKeyDown()` event handler.
           return (
-            DOM.li({ref: ref, key: index, className: classes},
-              DOM.a({href: "#", onClick: this.setActive.bind(this, index + 1)},
+            DOM.li({
+              ref: ref,
+              key: index,
+              className: classes},
+              DOM.a({
+                href: "#",
+                tabIndex: this.state.tabActive === index ? 0 : -1,
+                onClick: this.onClickTab.bind(this, index)},
                 title
               )
             )
           );
-        }.bind(this));
+        });
 
       return (
         DOM.nav({className: "tabs-navigation"},
           DOM.ul({className: "tabs-menu"},
-            menuItems
+            tabs
           )
         )
       );
     },
 
-    getSelectedPanel: function () {
-      let index = this.state.tabActive - 1;
-      let panel = this.props.children[index];
+    renderPanels: function () {
+      if (!this.props.children) {
+        throw new Error("There must be at least one Tab");
+      }
+
+      if (!Array.isArray(this.props.children)) {
+        this.props.children = [this.props.children];
+      }
+
+      let selectedIndex = this.state.tabActive;
+
+      let panels = this.props.children
+        .map(tab => {
+          return typeof tab === "function" ? tab() : tab;
+        }).filter(tab => {
+          return tab;
+        }).map((tab, index) => {
+          let selected = selectedIndex == index;
+
+          // Use 'visibility:hidden' + 'width/height:0' for hiding
+          // content of non-selected tab. It's faster (not sure why)
+          // than display:none and visibility:collapse.
+          let style = {
+            visibility: selected ? "visible" : "hidden",
+            height: selected ? "100%" : "0",
+            width: selected ? "100%" : "0",
+          };
+
+          return (
+            DOM.div({
+              key: index,
+              style: style,
+              className: "tab-panel-box"},
+              (selected || this.state.created[index]) ? tab : null
+            )
+          );
+        });
 
       return (
-        DOM.article({ref: "tab-panel", className: "tab-panel"},
-          panel
+        DOM.div({className: "panels"},
+          panels
         )
       );
     },
 
     render: function () {
       let classNames = ["tabs", this.props.className].join(" ");
 
       return (
         DOM.div({className: classNames},
-          this.getMenuItems(),
-          this.getSelectedPanel()
+          this.renderMenuItems(),
+          this.renderPanels()
         )
       );
     },
   });
 
   /**
    * Renders simple tab 'panel'.
    */
@@ -174,17 +287,17 @@ define(function (require, exports, modul
       title: React.PropTypes.string.isRequired,
       children: React.PropTypes.oneOfType([
         React.PropTypes.array,
         React.PropTypes.element
       ]).isRequired
     },
 
     render: function () {
-      return DOM.div({},
+      return DOM.div({className: "tab-panel"},
         this.props.children
       );
     }
   });
 
   // Exports from this module
   exports.TabPanel = Panel;
   exports.Tabs = Tabs;
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -15,16 +15,17 @@ support-files =
 [test_reps_grip-array.html]
 [test_reps_null.html]
 [test_reps_number.html]
 [test_reps_object.html]
 [test_reps_object-with-text.html]
 [test_reps_object-with-url.html]
 [test_reps_string.html]
 [test_reps_stylesheet.html]
+[test_reps_text-node.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
 [test_sidebar_toggle.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -0,0 +1,60 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test text-node rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - text-node</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+  try {
+    let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+    let { TextNode } = browserRequire("devtools/client/shared/components/reps/text-node");
+
+    let gripStub = {
+      "type": "object",
+      "class": "Text",
+      "actor": "server1.conn1.child1/obj50",
+      "extensible": true,
+      "frozen": false,
+      "sealed": false,
+      "ownPropertyLength": 0,
+      "preview": {
+        "kind": "DOMNode",
+        "nodeType": 3,
+        "nodeName": "#text",
+        "textContent": "hello world"
+      }
+    };
+
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+    is(renderedRep.type, TextNode.rep,
+      `Rep correctly selects ${TextNode.rep.displayName}`);
+
+    // Test rendering
+    const renderedComponent = renderComponent(TextNode.rep, { object: gripStub });
+    is(renderedComponent.className, "objectBox objectBox-textNode",
+      "TextNode rep has expected class names");
+    is(renderedComponent.textContent, `"hello world"`,
+      "TextNode rep has expected text content");
+  } catch (e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -100,16 +100,21 @@ body {
 }
 
 [timeline] #timeline-toolbar {
   display: flex;
 }
 
 /* The main animations container */
 
+#sidebar-panel-animationinspector {
+  height: 100%;
+  width: 100%;
+}
+
 #players {
   height: calc(100% - var(--toolbar-height));
   overflow-x: hidden;
   overflow-y: auto;
 }
 
 [empty] #players {
   display: none;
--- a/devtools/client/themes/computed.css
+++ b/devtools/client/themes/computed.css
@@ -3,35 +3,44 @@
  * 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/. */
 
 #sidebar-panel-computedview {
   margin: 0;
   display : flex;
   flex-direction: column;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 #sidebar-panel-computedview > .devtools-toolbar {
   display: flex;
 }
 
 #browser-style-checkbox {
   /* Bug 1200073 - extra space before the browser styles checkbox so
-     they aren't squished together in a small window. */
+     they aren't squished together in a small window. Put also
+     an extra space after. */
   margin-inline-start: 5px;
+  margin-inline-end: 5px;
+}
+
+#browser-style-checkbox-label {
+  margin-right: 5px;
+
+  /* Vertically center the 'Browser styles' checkbox in the
+     Computed panel with its label. */
+  display: flex;
+  align-items: center;
 }
 
 #propertyContainer {
   -moz-user-select: text;
-  overflow: auto;
+  overflow-y: auto;
+  overflow-x: hidden;
   flex: auto;
 }
 
 .row-striped {
   background: var(--theme-body-background);
 }
 
 .property-view-hidden,
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -3,20 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #sidebar-panel-fontinspector {
   margin: 0;
   display: flex;
   flex-direction: column;
   padding-bottom: 20px;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 #sidebar-panel-fontinspector > .devtools-toolbar {
   display: flex;
 }
 
 #font-container {
   overflow: auto;
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -1,15 +1,16 @@
 /* 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/ */
 
 #sidebar-panel-layoutview {
   display: block;
   overflow: auto;
+  height: 100%;
 }
 
 #layout-wrapper {
   /* The sidebar-panel is not focusable, this wrapper will catch click events in
      all the empty area around the layout-container */
   height: 100%;
 }
 
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -27,20 +27,17 @@
   font-size: 11px;
 }
 
 #sidebar-panel-ruleview {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 /* Rule View Toolbar */
 
 #ruleview-toolbar-container {
   display: flex;
   flex-direction: column;
   height: auto;
@@ -78,16 +75,17 @@
 }
 
 /* Rule View Container */
 
 #ruleview-container {
   -moz-user-select: text;
   overflow: auto;
   flex: auto;
+  height: 100%;
 }
 
 #ruleview-container.non-interactive {
   pointer-events: none;
   visibility: collapse;
   transition: visibility 0.25s;
 }
 
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -501,16 +501,17 @@
 .devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 /* In-tools sidebar */
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
+  height: 100%;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   -moz-appearance: none;
   background: transparent;
   padding: 0;
   border: 0;
 }
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -3,28 +3,31 @@
 /* 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";
 
 // React
 const {
+  createFactory,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link").VariablesViewLink);
 const { Grip } = require("devtools/client/shared/components/reps/grip");
 
 GripMessageBody.displayName = "GripMessageBody";
 
 GripMessageBody.propTypes = {
   grip: PropTypes.object.isRequired,
 };
 
 function GripMessageBody(props) {
   return Rep({
     object: props.grip,
+    objectLink: VariablesViewLink,
     defaultRep: Grip
   });
 }
 
 module.exports.GripMessageBody = GripMessageBody;
--- a/devtools/client/webconsole/new-console-output/test/components/head.js
+++ b/devtools/client/webconsole/new-console-output/test/components/head.js
@@ -34,17 +34,17 @@ testCommands.set("console.log()", {
   commandType: "consoleAPICall",
   // @TODO should output: foobar test
   expectedText: "\"foobar\"\"test\""
 });
 testCommands.set("new Date()", {
   command: "new Date(448156800000)",
   commandType: "evaluationResult",
   // @TODO should output: Date 1984-03-15T00:00:00.000Z
-  expectedText: "1984-03-15T00:00:00.000Z"
+  expectedText: "Date1984-03-15T00:00:00.000Z"
 });
 testCommands.set("pageError", {
   command: null,
   commandType: "pageError",
   expectedText: "ReferenceError: asdf is not defined"
 });
 
 function* getPacket(command, type = "evaluationResult") {
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -14,17 +14,16 @@ const { EnvironmentActor } = require("de
 const { FrameActor } = require("devtools/server/actors/frame");
 const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
 const { SourceActor, getSourceURL } = require("devtools/server/actors/source");
 const { DebuggerServer } = require("devtools/server/main");
 const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, dumpn, update, fetch } = DevToolsUtils;
 const promise = require("promise");
-const PromiseDebugging = require("PromiseDebugging");
 const xpcInspector = require("xpcInspector");
 const ScriptStore = require("./utils/ScriptStore");
 const { DevToolsWorker } = require("devtools/shared/worker/worker");
 const object = require("sdk/util/object");
 const { threadSpec } = require("devtools/shared/specs/script");
 
 const { defer, resolve, reject, all } = promise;
 
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -216,16 +216,17 @@ let SourceActor = ActorClassWithSpec(sou
       actor: this.actorID,
       generatedUrl: this.generatedSource ? this.generatedSource.url : null,
       url: this.url ? this.url.split(" -> ").pop() : null,
       addonID: this._addonID,
       addonPath: this._addonPath,
       isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
       isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
       isSourceMapped: this.isSourceMapped,
+      sourceMapURL: source ? source.sourceMapURL : null,
       introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
       introductionType: source ? source.introductionType : null
     };
   },
 
   disconnect: function () {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
--- a/devtools/shared/worker/loader.js
+++ b/devtools/shared/worker/loader.js
@@ -312,22 +312,16 @@ function WorkerDebuggerLoader(options) {
 }
 
 this.WorkerDebuggerLoader = WorkerDebuggerLoader;
 
 // The following APIs rely on the use of Components, and the worker debugger
 // does not provide alternative definitions for them. Consequently, they are
 // stubbed out both on the main thread and worker threads.
 
-var PromiseDebugging = {
-  getState: function () {
-    throw new Error("PromiseDebugging is not available in workers!");
-  }
-};
-
 var chrome = {
   CC: undefined,
   Cc: undefined,
   ChromeWorker: undefined,
   Cm: undefined,
   Ci: undefined,
   Cu: undefined,
   Cr: undefined,
@@ -491,17 +485,16 @@ this.worker = new WorkerDebuggerLoader({
     "reportError": reportError,
     "rpc": rpc,
     "setImmediate": setImmediate,
     "URL": URL,
   },
   loadSubScript: loadSubScript,
   modules: {
     "Debugger": Debugger,
-    "PromiseDebugging": PromiseDebugging,
     "Services": Object.create(null),
     "chrome": chrome,
     "xpcInspector": xpcInspector
   },
   paths: {
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "": "resource://gre/modules/commonjs/",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
--- a/docshell/test/browser/browser_timelineMarkers-frame-04.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-04.js
@@ -45,17 +45,21 @@ if (Services.prefs.getBoolPref("javascri
       markers = markers.filter(m => (m.name == "Javascript" &&
                                      m.causeName == "promise callback"));
       ok(markers.length > 0, "Found a Javascript marker");
 
       let frame = markers[0].stack;
       ok(frame.asyncParent !== null, "Parent frame has async parent");
       is(frame.asyncParent.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.asyncParent.functionDisplayName, "do_promise",
+      let asyncFrame = frame.asyncParent;
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted')
+        asyncFrame = asyncFrame.parent;
+      is(asyncFrame.functionDisplayName, "do_promise",
          "Async parent has correct function name");
     }
   }, {
     desc: "Async stack trace on Javascript marker with script",
     searchFor: (markers) => {
       return markers.some(m => (m.name == "Javascript" &&
                                 m.causeName == "promise callback"));
     },
@@ -66,15 +70,19 @@ if (Services.prefs.getBoolPref("javascri
       markers = markers.filter(m => (m.name == "Javascript" &&
                                      m.causeName == "promise callback"));
       ok(markers.length > 0, "Found a Javascript marker");
 
       let frame = markers[0].stack;
       ok(frame.asyncParent !== null, "Parent frame has async parent");
       is(frame.asyncParent.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.asyncParent.functionDisplayName, "do_promise_script",
+      let asyncFrame = frame.asyncParent;
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted')
+        asyncFrame = asyncFrame.parent;
+      is(asyncFrame.functionDisplayName, "do_promise_script",
          "Async parent has correct function name");
     }
   });
 }
 
 timelineContentTest(TESTS);
--- a/docshell/test/browser/browser_timelineMarkers-frame-05.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js
@@ -86,20 +86,32 @@ if (Services.prefs.getBoolPref("javascri
     searchFor: "ConsoleTime",
     setup: function(docShell) {
       let resolver = makePromise();
       resolvePromise(resolver);
     },
     check: function(markers) {
       markers = markers.filter(m => m.name == "ConsoleTime");
       ok(markers.length > 0, "Promise marker includes stack");
-
+      ok(markers[0].stack.functionDisplayName == "testConsoleTime",
+         "testConsoleTime is on the stack");
       let frame = markers[0].endStack;
-      ok(frame.parent.asyncParent !== null, "Parent frame has async parent");
-      is(frame.parent.asyncParent.asyncCause, "promise callback",
+      ok(frame.functionDisplayName == "testConsoleTimeEnd",
+         "testConsoleTimeEnd is on the stack");
+
+      frame = frame.parent;
+      ok(frame.functionDisplayName == "makePromise/<",
+         "makePromise/< is on the stack");
+      let asyncFrame = frame.asyncParent;
+      ok(asyncFrame !== null, "Frame has async parent");
+      is(asyncFrame.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.parent.asyncParent.functionDisplayName, "makePromise",
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted') {
+        asyncFrame = asyncFrame.parent;
+      }
+      is(asyncFrame.functionDisplayName, "makePromise",
          "Async parent has correct function name");
     }
   });
 }
 
 timelineContentTest(TESTS);
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1454,19 +1454,16 @@ nsIDocument::nsIDocument()
     mGetUserFontSetCalled(false),
     mPostedFlushUserFontSet(false),
     mPartID(0),
     mDidFireDOMContentLoaded(true),
     mHasScrollLinkedEffect(false),
     mUserHasInteracted(false)
 {
   SetIsDocument();
-  if (IsStyledByServo()) {
-    SetFlags(NODE_IS_DIRTY_FOR_SERVO | NODE_HAS_DIRTY_DESCENDANTS_FOR_SERVO);
-  }
 
   PR_INIT_CLIST(&mDOMMediaQueryLists);
 }
 
 // NOTE! nsDocument::operator new() zeroes out all members, so don't
 // bother initializing members to 0.
 
 nsDocument::nsDocument(const char* aContentType)
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -138,53 +138,64 @@ ThrowNoSetterArg(JSContext* aCx, prototy
 {
   nsPrintfCString errorMessage("%s attribute setter",
                                NamesOfInterfacesWithProtos(aProtoId));
   return ThrowErrorMessage(aCx, MSG_MISSING_ARGUMENTS, errorMessage.get());
 }
 
 } // namespace dom
 
-struct ErrorResult::Message {
-  Message() { MOZ_COUNT_CTOR(ErrorResult::Message); }
-  ~Message() { MOZ_COUNT_DTOR(ErrorResult::Message); }
+namespace binding_danger {
+
+template<typename CleanupPolicy>
+struct TErrorResult<CleanupPolicy>::Message {
+  Message() { MOZ_COUNT_CTOR(TErrorResult::Message); }
+  ~Message() { MOZ_COUNT_DTOR(TErrorResult::Message); }
 
   nsTArray<nsString> mArgs;
   dom::ErrNum mErrorNumber;
 
   bool HasCorrectNumberOfArguments()
   {
     return GetErrorArgCount(mErrorNumber) == mArgs.Length();
   }
 };
 
+template<typename CleanupPolicy>
 nsTArray<nsString>&
-ErrorResult::CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType)
+TErrorResult<CleanupPolicy>::CreateErrorMessageHelper(const dom::ErrNum errorNumber,
+                                                      nsresult errorType)
 {
+  AssertInOwningThread();
   mResult = errorType;
 
   mMessage = new Message();
   mMessage->mErrorNumber = errorNumber;
   return mMessage->mArgs;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SerializeMessage(IPC::Message* aMsg) const
+TErrorResult<CleanupPolicy>::SerializeMessage(IPC::Message* aMsg) const
 {
   using namespace IPC;
+  AssertInOwningThread();
   MOZ_ASSERT(mUnionState == HasMessage);
   MOZ_ASSERT(mMessage);
   WriteParam(aMsg, mMessage->mArgs);
   WriteParam(aMsg, mMessage->mErrorNumber);
 }
 
+template<typename CleanupPolicy>
 bool
-ErrorResult::DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter)
+TErrorResult<CleanupPolicy>::DeserializeMessage(const IPC::Message* aMsg,
+                                                PickleIterator* aIter)
 {
   using namespace IPC;
+  AssertInOwningThread();
   nsAutoPtr<Message> readMessage(new Message());
   if (!ReadParam(aMsg, aIter, &readMessage->mArgs) ||
       !ReadParam(aMsg, aIter, &readMessage->mErrorNumber)) {
     return false;
   }
   if (!readMessage->HasCorrectNumberOfArguments()) {
     return false;
   }
@@ -192,19 +203,21 @@ ErrorResult::DeserializeMessage(const IP
   MOZ_ASSERT(mUnionState == HasNothing);
   mMessage = readMessage.forget();
 #ifdef DEBUG
   mUnionState = HasMessage;
 #endif // DEBUG
   return true;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingExceptionWithMessage(JSContext* aCx)
+TErrorResult<CleanupPolicy>::SetPendingExceptionWithMessage(JSContext* aCx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMessage, "SetPendingExceptionWithMessage() can be called only once");
   MOZ_ASSERT(mUnionState == HasMessage);
 
   Message* message = mMessage;
   MOZ_RELEASE_ASSERT(message->HasCorrectNumberOfArguments());
   const uint32_t argCount = message->mArgs.Length();
   const char16_t* args[JS::MaxNumErrorArguments + 1];
   for (uint32_t i = 0; i < argCount; ++i) {
@@ -215,55 +228,61 @@ ErrorResult::SetPendingExceptionWithMess
   JS_ReportErrorNumberUCArray(aCx, dom::GetErrorMessage, nullptr,
                               static_cast<const unsigned>(message->mErrorNumber),
                               argCount > 0 ? args : nullptr);
 
   ClearMessage();
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearMessage()
+TErrorResult<CleanupPolicy>::ClearMessage()
 {
+  AssertInOwningThread();
   MOZ_ASSERT(IsErrorWithMessage());
   delete mMessage;
   mMessage = nullptr;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn)
+TErrorResult<CleanupPolicy>::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to throw a JS exception?");
 
   ClearUnionData();
 
   // Make sure mJSException is initialized _before_ we try to root it.  But
   // don't set it to exn yet, because we don't want to do that until after we
   // root.
   mJSException.setUndefined();
-  if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) {
+  if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) {
     // Don't use NS_ERROR_DOM_JS_EXCEPTION, because that indicates we have
     // in fact rooted mJSException.
     mResult = NS_ERROR_OUT_OF_MEMORY;
   } else {
     mJSException = exn;
     mResult = NS_ERROR_DOM_JS_EXCEPTION;
 #ifdef DEBUG
     mUnionState = HasJSException;
 #endif // DEBUG
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingJSException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingJSException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(!mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to handle JS exceptions?");
   MOZ_ASSERT(mUnionState == HasJSException);
 
   JS::Rooted<JS::Value> exception(cx, mJSException);
   if (JS_WrapValue(cx, &exception)) {
     JS_SetPendingException(cx, exception);
   }
@@ -273,140 +292,160 @@ ErrorResult::SetPendingJSException(JSCon
   js::RemoveRawValueRoot(cx, &mJSException);
 
   mResult = NS_OK;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
-struct ErrorResult::DOMExceptionInfo {
+template<typename CleanupPolicy>
+struct TErrorResult<CleanupPolicy>::DOMExceptionInfo {
   DOMExceptionInfo(nsresult rv, const nsACString& message)
     : mMessage(message)
     , mRv(rv)
   {}
 
   nsCString mMessage;
   nsresult mRv;
 };
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SerializeDOMExceptionInfo(IPC::Message* aMsg) const
+TErrorResult<CleanupPolicy>::SerializeDOMExceptionInfo(IPC::Message* aMsg) const
 {
   using namespace IPC;
+  AssertInOwningThread();
   MOZ_ASSERT(mDOMExceptionInfo);
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo);
   WriteParam(aMsg, mDOMExceptionInfo->mMessage);
   WriteParam(aMsg, mDOMExceptionInfo->mRv);
 }
 
+template<typename CleanupPolicy>
 bool
-ErrorResult::DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter)
+TErrorResult<CleanupPolicy>::DeserializeDOMExceptionInfo(const IPC::Message* aMsg,
+                                                         PickleIterator* aIter)
 {
   using namespace IPC;
+  AssertInOwningThread();
   nsCString message;
   nsresult rv;
   if (!ReadParam(aMsg, aIter, &message) ||
       !ReadParam(aMsg, aIter, &rv)) {
     return false;
   }
 
   MOZ_ASSERT(mUnionState == HasNothing);
   MOZ_ASSERT(IsDOMException());
   mDOMExceptionInfo = new DOMExceptionInfo(rv, message);
 #ifdef DEBUG
   mUnionState = HasDOMExceptionInfo;
 #endif // DEBUG
   return true;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ThrowDOMException(nsresult rv, const nsACString& message)
+TErrorResult<CleanupPolicy>::ThrowDOMException(nsresult rv,
+                                               const nsACString& message)
 {
+  AssertInOwningThread();
   ClearUnionData();
 
   mResult = NS_ERROR_DOM_DOMEXCEPTION;
   mDOMExceptionInfo = new DOMExceptionInfo(rv, message);
 #ifdef DEBUG
   mUnionState = HasDOMExceptionInfo;
 #endif
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingDOMException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingDOMException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mDOMExceptionInfo,
              "SetPendingDOMException() can be called only once");
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo);
 
   dom::Throw(cx, mDOMExceptionInfo->mRv, mDOMExceptionInfo->mMessage);
 
   ClearDOMExceptionInfo();
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearDOMExceptionInfo()
+TErrorResult<CleanupPolicy>::ClearDOMExceptionInfo()
 {
+  AssertInOwningThread();
   MOZ_ASSERT(IsDOMException());
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo || !mDOMExceptionInfo);
   delete mDOMExceptionInfo;
   mDOMExceptionInfo = nullptr;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearUnionData()
+TErrorResult<CleanupPolicy>::ClearUnionData()
 {
+  AssertInOwningThread();
   if (IsJSException()) {
     JSContext* cx = nsContentUtils::RootingCx();
     MOZ_ASSERT(cx);
     mJSException.setUndefined();
     js::RemoveRawValueRoot(cx, &mJSException);
 #ifdef DEBUG
     mUnionState = HasNothing;
 #endif // DEBUG
   } else if (IsErrorWithMessage()) {
     ClearMessage();
   } else if (IsDOMException()) {
     ClearDOMExceptionInfo();
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingGenericErrorException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingGenericErrorException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(!IsErrorWithMessage());
   MOZ_ASSERT(!IsJSException());
   MOZ_ASSERT(!IsDOMException());
   dom::Throw(cx, ErrorCode());
   mResult = NS_OK;
 }
 
-ErrorResult&
-ErrorResult::operator=(ErrorResult&& aRHS)
+template<typename CleanupPolicy>
+TErrorResult<CleanupPolicy>&
+TErrorResult<CleanupPolicy>::operator=(TErrorResult<CleanupPolicy>&& aRHS)
 {
+  AssertInOwningThread();
+  aRHS.AssertInOwningThread();
   // Clear out any union members we may have right now, before we
   // start writing to it.
   ClearUnionData();
 
 #ifdef DEBUG
   mMightHaveUnreportedJSException = aRHS.mMightHaveUnreportedJSException;
   aRHS.mMightHaveUnreportedJSException = false;
 #endif
   if (aRHS.IsErrorWithMessage()) {
     mMessage = aRHS.mMessage;
     aRHS.mMessage = nullptr;
   } else if (aRHS.IsJSException()) {
     JSContext* cx = nsContentUtils::RootingCx();
     MOZ_ASSERT(cx);
     mJSException.setUndefined();
-    if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) {
+    if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) {
       MOZ_CRASH("Could not root mJSException, we're about to OOM");
     }
     mJSException = aRHS.mJSException;
     aRHS.mJSException.setUndefined();
     js::RemoveRawValueRoot(cx, &aRHS.mJSException);
   } else if (aRHS.IsDOMException()) {
     mDOMExceptionInfo = aRHS.mDOMExceptionInfo;
     aRHS.mDOMExceptionInfo = nullptr;
@@ -422,19 +461,23 @@ ErrorResult::operator=(ErrorResult&& aRH
 
   // Note: It's important to do this last, since this affects the condition
   // checks above!
   mResult = aRHS.mResult;
   aRHS.mResult = NS_OK;
   return *this;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::CloneTo(ErrorResult& aRv) const
+TErrorResult<CleanupPolicy>::CloneTo(TErrorResult& aRv) const
 {
+  AssertInOwningThread();
+  aRv.AssertInOwningThread();
+
   aRv.ClearUnionData();
   aRv.mResult = mResult;
 #ifdef DEBUG
   aRv.mMightHaveUnreportedJSException = mMightHaveUnreportedJSException;
 #endif
 
   if (IsErrorWithMessage()) {
 #ifdef DEBUG
@@ -454,29 +497,33 @@ ErrorResult::CloneTo(ErrorResult& aRv) c
     aRv.mUnionState = HasJSException;
 #endif
     JSContext* cx = nsContentUtils::RootingCx();
     JS::Rooted<JS::Value> exception(cx, mJSException);
     aRv.ThrowJSException(cx, exception);
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SuppressException()
+TErrorResult<CleanupPolicy>::SuppressException()
 {
+  AssertInOwningThread();
   WouldReportJSException();
   ClearUnionData();
   // We don't use AssignErrorCode, because we want to override existing error
   // states, which AssignErrorCode is not allowed to do.
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingException(JSContext* cx)
 {
+  AssertInOwningThread();
   if (IsUncatchableException()) {
     // Nuke any existing exception on cx, to make sure we're uncatchable.
     JS_ClearPendingException(cx);
     // Don't do any reporting.  Just return, to create an
     // uncatchable exception.
     mResult = NS_OK;
     return;
   }
@@ -496,42 +543,52 @@ ErrorResult::SetPendingException(JSConte
   }
   if (IsDOMException()) {
     SetPendingDOMException(cx);
     return;
   }
   SetPendingGenericErrorException(cx);
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::StealExceptionFromJSContext(JSContext* cx)
+TErrorResult<CleanupPolicy>::StealExceptionFromJSContext(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to throw a JS exception?");
 
   JS::Rooted<JS::Value> exn(cx);
   if (!JS_GetPendingException(cx, &exn)) {
     ThrowUncatchableException();
     return;
   }
 
   ThrowJSException(cx, exn);
   JS_ClearPendingException(cx);
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::NoteJSContextException(JSContext* aCx)
+TErrorResult<CleanupPolicy>::NoteJSContextException(JSContext* aCx)
 {
+  AssertInOwningThread();
   if (JS_IsExceptionPending(aCx)) {
     mResult = NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT;
   } else {
     mResult = NS_ERROR_UNCATCHABLE_EXCEPTION;
   }
 }
 
+template class TErrorResult<JustAssertCleanupPolicy>;
+template class TErrorResult<AssertAndSuppressCleanupPolicy>;
+template class TErrorResult<JustSuppressCleanupPolicy>;
+
+} // namespace binding_danger
+
 namespace dom {
 
 bool
 DefineConstants(JSContext* cx, JS::Handle<JSObject*> obj,
                 const ConstantSpec* cs)
 {
   JS::Rooted<JS::Value> value(cx);
   for (; cs->name; ++cs) {
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -101,17 +101,17 @@ IsNonProxyDOMClass(const js::Class* clas
 }
 
 inline bool
 IsNonProxyDOMClass(const JSClass* clasp)
 {
   return IsNonProxyDOMClass(js::Valueify(clasp));
 }
 
-// Returns true if the JSClass is used for DOM interface and interface 
+// Returns true if the JSClass is used for DOM interface and interface
 // prototype objects.
 inline bool
 IsDOMIfaceAndProtoClass(const JSClass* clasp)
 {
   return clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS;
 }
 
 inline bool
@@ -2006,16 +2006,22 @@ private:
                     "Offset of mLength should match");
       static_assert(offsetof(FakeString, mFlags) ==
                       offsetof(StringAsserter, mFlags),
                     "Offset of mFlags should match");
     }
   };
 };
 
+class FastErrorResult :
+    public mozilla::binding_danger::TErrorResult<
+      mozilla::binding_danger::JustAssertCleanupPolicy>
+{
+};
+
 } // namespace binding_detail
 
 enum StringificationBehavior {
   eStringify,
   eEmpty,
   eNull
 };
 
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -5207,32 +5207,32 @@ def getJSToNativeConversionInfo(type, de
                     globalObj = js::GetGlobalForObjectCrossCompartment(unwrappedVal);
                     """,
                     sourceDescription=sourceDescription)
             else:
                 getPromiseGlobal = ""
 
             templateBody = fill(
                 """
-                { // Scope for our GlobalObject, ErrorResult, JSAutoCompartment,
+                { // Scope for our GlobalObject, FastErrorResult, JSAutoCompartment,
                   // etc.
 
                   JS::Rooted<JSObject*> globalObj(cx, JS::CurrentGlobalOrNull(cx));
                   $*{getPromiseGlobal}
                   JSAutoCompartment ac(cx, globalObj);
                   GlobalObject promiseGlobal(cx, globalObj);
                   if (promiseGlobal.Failed()) {
                     $*{exceptionCode}
                   }
 
                   JS::Rooted<JS::Value> valueToResolve(cx, $${val});
                   if (!JS_WrapValue(cx, &valueToResolve)) {
                     $*{exceptionCode}
                   }
-                  ErrorResult promiseRv;
+                  binding_detail::FastErrorResult promiseRv;
                 #ifdef SPIDERMONKEY_PROMISE
                   nsCOMPtr<nsIGlobalObject> global =
                     do_QueryInterface(promiseGlobal.GetAsSupports());
                   if (!global) {
                     promiseRv.Throw(NS_ERROR_UNEXPECTED);
                     promiseRv.MaybeSetPendingException(cx);
                     $*{exceptionCode}
                   }
@@ -6917,17 +6917,17 @@ class CGCallGenerator(CGThing):
         elif result is not None:
             assert resultOutParam is None
             call = CGWrapper(call, pre=resultVar + " = ")
 
         call = CGWrapper(call, post=";\n")
         self.cgRoot.append(call)
 
         if isFallible:
-            self.cgRoot.prepend(CGGeneric("ErrorResult rv;\n"))
+            self.cgRoot.prepend(CGGeneric("binding_detail::FastErrorResult rv;\n"))
             self.cgRoot.append(CGGeneric(dedent(
                 """
                 if (MOZ_UNLIKELY(rv.MaybeSetPendingException(cx))) {
                   return false;
                 }
                 """)))
 
         self.cgRoot.append(CGGeneric("MOZ_ASSERT(!JS_IsExceptionPending(cx));\n"))
@@ -8432,17 +8432,17 @@ class CGEnumerateHook(CGAbstractBindingM
         # Our "self" is actually the "obj" argument in this case, not the thisval.
         CGAbstractBindingMethod.__init__(
             self, descriptor, ENUMERATE_HOOK_NAME,
             args, getThisObj="", callArgs="")
 
     def generate_code(self):
         return CGGeneric(dedent("""
             AutoTArray<nsString, 8> names;
-            ErrorResult rv;
+            binding_detail::FastErrorResult rv;
             self->GetOwnPropertyNames(cx, names, rv);
             if (rv.MaybeSetPendingException(cx)) {
               return false;
             }
             bool dummy;
             for (uint32_t i = 0; i < names.Length(); ++i) {
               if (!JS_HasUCProperty(cx, obj, names[i].get(), names[i].Length(), &dummy)) {
                 return false;
@@ -10525,17 +10525,17 @@ class CGEnumerateOwnPropertiesViaGetOwnP
         CGAbstractBindingMethod.__init__(self, descriptor,
                                          "EnumerateOwnPropertiesViaGetOwnPropertyNames",
                                          args, getThisObj="",
                                          callArgs="")
 
     def generate_code(self):
         return CGGeneric(dedent("""
             AutoTArray<nsString, 8> names;
-            ErrorResult rv;
+            binding_detail::FastErrorResult rv;
             self->GetOwnPropertyNames(cx, names, rv);
             if (rv.MaybeSetPendingException(cx)) {
               return false;
             }
             // OK to pass null as "proxy" because it's ignored if
             // shadowPrototypeProperties is true
             return AppendNamedPropertyIds(cx, nullptr, names, true, props);
             """))
--- a/dom/bindings/ErrorResult.h
+++ b/dom/bindings/ErrorResult.h
@@ -1,41 +1,45 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 /**
- * A struct for tracking exceptions that need to be thrown to JS.
+ * A set of structs for tracking exceptions that need to be thrown to JS:
+ * ErrorResult and IgnoredErrorResult.
  *
- * Conceptually, an ErrorResult represents either success or an exception in the
+ * Conceptually, these structs represent either success or an exception in the
  * process of being thrown.  This means that a failing ErrorResult _must_ be
  * handled in one of the following ways before coming off the stack:
  *
  * 1) Suppressed via SuppressException().
  * 2) Converted to a pure nsresult return value via StealNSResult().
  * 3) Converted to an actual pending exception on a JSContext via
  *    MaybeSetPendingException.
  * 4) Converted to an exception JS::Value (probably to then reject a Promise
  *    with) via dom::ToJSValue.
+ *
+ * An IgnoredErrorResult will automatically do the first of those four things.
  */
 
 #ifndef mozilla_ErrorResult_h
 #define mozilla_ErrorResult_h
 
 #include <stdarg.h>
 
 #include "js/GCAnnotations.h"
 #include "js/Value.h"
 #include "nscore.h"
 #include "nsStringGlue.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Move.h"
 #include "nsTArray.h"
+#include "nsISupportsImpl.h"
 
 namespace IPC {
 class Message;
 template <typename> struct ParamTraits;
 } // namespace IPC
 class PickleIterator;
 
 namespace mozilla {
@@ -82,82 +86,107 @@ struct StringArrayAppender
     }
     aArgs.AppendElement(aFirst);
     Append(aArgs, aCount - 1, Forward<Ts>(aOtherArgs)...);
   }
 };
 
 } // namespace dom
 
-class ErrorResult {
+class ErrorResult;
+
+namespace binding_danger {
+
+/**
+ * Templated implementation class for various ErrorResult-like things.  The
+ * instantiations differ only in terms of their cleanup policies (used in the
+ * destructor), which they can specify via the template argument.  Note that
+ * this means it's safe to reinterpret_cast between the instantiations unless
+ * you plan to invoke the destructor through such a cast pointer.
+ *
+ * A cleanup policy consists of two booleans: whether to assert that we've been
+ * reported or suppressed, and whether to then go ahead and suppress the
+ * exception.
+ */
+template<typename CleanupPolicy>
+class TErrorResult {
 public:
-  ErrorResult()
+  TErrorResult()
     : mResult(NS_OK)
 #ifdef DEBUG
     , mMightHaveUnreportedJSException(false)
     , mUnionState(HasNothing)
 #endif
   {
   }
 
-#ifdef DEBUG
-  ~ErrorResult() {
-    // Consumers should have called one of MaybeSetPendingException
-    // (possibly via ToJSValue), StealNSResult, and SuppressException
-    MOZ_ASSERT(!Failed());
-    MOZ_ASSERT(!mMightHaveUnreportedJSException);
-    MOZ_ASSERT(mUnionState == HasNothing);
+  ~TErrorResult() {
+    AssertInOwningThread();
+
+    if (CleanupPolicy::assertHandled) {
+      // Consumers should have called one of MaybeSetPendingException
+      // (possibly via ToJSValue), StealNSResult, and SuppressException
+      AssertReportedOrSuppressed();
+    }
+
+    if (CleanupPolicy::suppress) {
+      SuppressException();
+    }
+
+    // And now assert that we're in a good final state.
+    AssertReportedOrSuppressed();
   }
-#endif // DEBUG
 
-  ErrorResult(ErrorResult&& aRHS)
+  TErrorResult(TErrorResult&& aRHS)
     // Initialize mResult and whatever else we need to default-initialize, so
     // the ClearUnionData call in our operator= will do the right thing
     // (nothing).
-    : ErrorResult()
+    : TErrorResult()
   {
     *this = Move(aRHS);
   }
-  ErrorResult& operator=(ErrorResult&& aRHS);
+  TErrorResult& operator=(TErrorResult&& aRHS);
 
-  explicit ErrorResult(nsresult aRv)
-    : ErrorResult()
+  explicit TErrorResult(nsresult aRv)
+    : TErrorResult()
   {
     AssignErrorCode(aRv);
   }
 
+  operator ErrorResult&();
+
   void Throw(nsresult rv) {
     MOZ_ASSERT(NS_FAILED(rv), "Please don't try throwing success");
     AssignErrorCode(rv);
   }
 
-  // Duplicate our current state on the given ErrorResult object.  Any existing
-  // errors or messages on the target will be suppressed before cloning.  Our
-  // own error state remains unchanged.
-  void CloneTo(ErrorResult& aRv) const;
+  // Duplicate our current state on the given TErrorResult object.  Any
+  // existing errors or messages on the target will be suppressed before
+  // cloning.  Our own error state remains unchanged.
+  void CloneTo(TErrorResult& aRv) const;
 
   // Use SuppressException when you want to suppress any exception that might be
-  // on the ErrorResult.  After this call, the ErrorResult will be back a "no
+  // on the TErrorResult.  After this call, the TErrorResult will be back a "no
   // exception thrown" state.
   void SuppressException();
 
-  // Use StealNSResult() when you want to safely convert the ErrorResult to an
-  // nsresult that you will then return to a caller.  This will
+  // Use StealNSResult() when you want to safely convert the TErrorResult to
+  // an nsresult that you will then return to a caller.  This will
   // SuppressException(), since there will no longer be a way to report it.
   nsresult StealNSResult() {
     nsresult rv = ErrorCode();
     SuppressException();
     return rv;
   }
 
-  // Use MaybeSetPendingException to convert an ErrorResult to a pending
+  // Use MaybeSetPendingException to convert a TErrorResult to a pending
   // exception on the given JSContext.  This is the normal "throw an exception"
   // codepath.
   //
-  // The return value is false if the ErrorResult represents success, true
+  // The return value is false if the TErrorResult represents success, true
   // otherwise.  This does mean that in JSAPI method implementations you can't
   // just use this as |return rv.MaybeSetPendingException(cx)| (though you could
   // |return !rv.MaybeSetPendingException(cx)|), but in practice pretty much any
   // consumer would want to do some more work on the success codepath.  So
   // instead the way you use this is:
   //
   //   if (rv.MaybeSetPendingException(cx)) {
   //     bail out here
@@ -168,31 +197,31 @@ public:
   // want to pay the price of a function call in some of the consumers of this
   // method in the common case.
   //
   // Note that a true return value does NOT mean there is now a pending
   // exception on aCx, due to uncatchable exceptions.  It should still be
   // considered equivalent to a JSAPI failure in terms of what callers should do
   // after true is returned.
   //
-  // After this call, the ErrorResult will no longer return true from Failed(),
+  // After this call, the TErrorResult will no longer return true from Failed(),
   // since the exception will have moved to the JSContext.
   bool MaybeSetPendingException(JSContext* cx)
   {
     WouldReportJSException();
     if (!Failed()) {
       return false;
     }
 
     SetPendingException(cx);
     return true;
   }
 
   // Use StealExceptionFromJSContext to convert a pending exception on a
-  // JSContext to an ErrorResult.  This function must be called only when a
+  // JSContext to a TErrorResult.  This function must be called only when a
   // JSAPI operation failed.  It assumes that lack of pending exception on the
   // JSContext means an uncatchable exception was thrown.
   //
   // Codepaths that might call this method must call MightThrowJSException even
   // if the relevant JSAPI calls do not fail.
   //
   // When this function returns, JS_IsExceptionPending(cx) will definitely be
   // false.
@@ -210,42 +239,42 @@ public:
   {
     ThrowErrorWithMessage<errorNumber>(NS_ERROR_RANGE_ERR,
                                        Forward<Ts>(messageArgs)...);
   }
 
   bool IsErrorWithMessage() const { return ErrorCode() == NS_ERROR_TYPE_ERR || ErrorCode() == NS_ERROR_RANGE_ERR; }
 
   // Facilities for throwing a preexisting JS exception value via this
-  // ErrorResult.  The contract is that any code which might end up calling
+  // TErrorResult.  The contract is that any code which might end up calling
   // ThrowJSException() or StealExceptionFromJSContext() must call
   // MightThrowJSException() even if no exception is being thrown.  Code that
-  // conditionally calls ToJSValue on this ErrorResult only if Failed() must
-  // first call WouldReportJSException even if this ErrorResult has not failed.
+  // conditionally calls ToJSValue on this TErrorResult only if Failed() must
+  // first call WouldReportJSException even if this TErrorResult has not failed.
   //
   // The exn argument to ThrowJSException can be in any compartment.  It does
   // not have to be in the compartment of cx.  If someone later uses it, they
   // will wrap it into whatever compartment they're working in, as needed.
   void ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn);
   bool IsJSException() const { return ErrorCode() == NS_ERROR_DOM_JS_EXCEPTION; }
 
   // Facilities for throwing a DOMException.  If an empty message string is
   // passed to ThrowDOMException, the default message string for the given
   // nsresult will be used.  The passed-in string must be UTF-8.  The nsresult
   // passed in must be one we create DOMExceptions for; otherwise you may get an
   // XPConnect Exception.
   void ThrowDOMException(nsresult rv, const nsACString& message = EmptyCString());
   bool IsDOMException() const { return ErrorCode() == NS_ERROR_DOM_DOMEXCEPTION; }
 
-  // Flag on the ErrorResult that whatever needs throwing has been
+  // Flag on the TErrorResult that whatever needs throwing has been
   // thrown on the JSContext already and we should not mess with it.
   // If nothing was thrown, this becomes an uncatchable exception.
   void NoteJSContextException(JSContext* aCx);
 
-  // Check whether the ErrorResult says to just throw whatever is on
+  // Check whether the TErrorResult says to just throw whatever is on
   // the JSContext already.
   bool IsJSContextException() {
     return ErrorCode() == NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT;
   }
 
   // Support for uncatchable exceptions.
   void ThrowUncatchableException() {
     Throw(NS_ERROR_UNCATCHABLE_EXCEPTION);
@@ -301,24 +330,25 @@ private:
   enum UnionState {
     HasMessage,
     HasDOMExceptionInfo,
     HasJSException,
     HasNothing
   };
 #endif // DEBUG
 
+  friend struct IPC::ParamTraits<TErrorResult>;
   friend struct IPC::ParamTraits<ErrorResult>;
   void SerializeMessage(IPC::Message* aMsg) const;
   bool DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter);
 
   void SerializeDOMExceptionInfo(IPC::Message* aMsg) const;
   bool DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter);
 
-  // Helper method that creates a new Message for this ErrorResult,
+  // Helper method that creates a new Message for this TErrorResult,
   // and returns the arguments array from that Message.
   nsTArray<nsString>& CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType);
 
   template<dom::ErrNum errorNumber, typename... Ts>
   void ThrowErrorWithMessage(nsresult errorType, Ts&&... messageArgs)
   {
 #if defined(DEBUG) && (defined(__clang__) || defined(__GNUC__))
     static_assert(dom::ErrorFormatNumArgs[errorNumber] == sizeof...(messageArgs),
@@ -331,16 +361,22 @@ private:
     uint16_t argCount = dom::GetErrorArgCount(errorNumber);
     dom::StringArrayAppender::Append(messageArgsArray, argCount,
                                      Forward<Ts>(messageArgs)...);
 #ifdef DEBUG
     mUnionState = HasMessage;
 #endif // DEBUG
   }
 
+  MOZ_ALWAYS_INLINE void AssertInOwningThread() const {
+#ifdef DEBUG
+    NS_ASSERT_OWNINGTHREAD(TErrorResult);
+#endif
+  }
+
   void AssignErrorCode(nsresult aRv) {
     MOZ_ASSERT(aRv != NS_ERROR_TYPE_ERR, "Use ThrowTypeError()");
     MOZ_ASSERT(aRv != NS_ERROR_RANGE_ERR, "Use ThrowRangeError()");
     MOZ_ASSERT(!IsErrorWithMessage(), "Don't overwrite errors with message");
     MOZ_ASSERT(aRv != NS_ERROR_DOM_JS_EXCEPTION, "Use ThrowJSException()");
     MOZ_ASSERT(!IsJSException(), "Don't overwrite JS exceptions");
     MOZ_ASSERT(aRv != NS_ERROR_DOM_DOMEXCEPTION, "Use ThrowDOMException()");
     MOZ_ASSERT(!IsDOMException(), "Don't overwrite DOM exceptions");
@@ -366,16 +402,22 @@ private:
   void SetPendingException(JSContext* cx);
 
   // Methods for setting various specific kinds of pending exceptions.
   void SetPendingExceptionWithMessage(JSContext* cx);
   void SetPendingJSException(JSContext* cx);
   void SetPendingDOMException(JSContext* cx);
   void SetPendingGenericErrorException(JSContext* cx);
 
+  MOZ_ALWAYS_INLINE void AssertReportedOrSuppressed()
+  {
+    MOZ_ASSERT(!Failed());
+    MOZ_ASSERT(!mMightHaveUnreportedJSException);
+    MOZ_ASSERT(mUnionState == HasNothing);
+  }
 
   // Special values of mResult:
   // NS_ERROR_TYPE_ERR -- ThrowTypeError() called on us.
   // NS_ERROR_RANGE_ERR -- ThrowRangeError() called on us.
   // NS_ERROR_DOM_JS_EXCEPTION -- ThrowJSException() called on us.
   // NS_ERROR_UNCATCHABLE_EXCEPTION -- ThrowUncatchableException called on us.
   // NS_ERROR_DOM_DOMEXCEPTION -- ThrowDOMException() called on us.
   nsresult mResult;
@@ -399,32 +441,95 @@ private:
   // for assertion purposes.
   bool mMightHaveUnreportedJSException;
 
   // Used to keep track of what's stored in our union right now.  Note
   // that this may be set to HasNothing even if our mResult suggests
   // we should have something, if we have already cleaned up the
   // something.
   UnionState mUnionState;
+
+  // The thread that created this TErrorResult
+  NS_DECL_OWNINGTHREAD;
 #endif
 
   // Not to be implemented, to make sure people always pass this by
   // reference, not by value.
+  TErrorResult(const TErrorResult&) = delete;
+  void operator=(const TErrorResult&) = delete;
+};
+
+struct JustAssertCleanupPolicy {
+  static const bool assertHandled = true;
+  static const bool suppress = false;
+};
+
+struct AssertAndSuppressCleanupPolicy {
+  static const bool assertHandled = true;
+  static const bool suppress = true;
+};
+
+struct JustSuppressCleanupPolicy {
+  static const bool assertHandled = false;
+  static const bool suppress = true;
+};
+
+} // namespace binding_danger
+
+// A class people should normally use on the stack when they plan to actually
+// do something with the exception.
+class ErrorResult :
+    public binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy>
+{
+  typedef binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy> BaseErrorResult;
+
+public:
+  ErrorResult()
+    : BaseErrorResult()
+  {}
+
+  ErrorResult(ErrorResult&& aRHS)
+    : BaseErrorResult(Move(aRHS))
+  {}
+
+  explicit ErrorResult(nsresult aRv)
+    : BaseErrorResult(aRv)
+  {}
+
+  void operator=(nsresult rv)
+  {
+    BaseErrorResult::operator=(rv);
+  }
+
+  ErrorResult& operator=(ErrorResult&& aRHS)
+  {
+    BaseErrorResult::operator=(Move(aRHS));
+    return *this;
+  }
+
+private:
+  // Not to be implemented, to make sure people always pass this by
+  // reference, not by value.
   ErrorResult(const ErrorResult&) = delete;
   void operator=(const ErrorResult&) = delete;
 };
 
-// A class for use when an ErrorResult should just automatically be ignored.
-class IgnoredErrorResult : public ErrorResult
+template<typename CleanupPolicy>
+binding_danger::TErrorResult<CleanupPolicy>::operator ErrorResult&()
 {
-public:
-  ~IgnoredErrorResult()
-  {
-    SuppressException();
-  }
+  return *static_cast<ErrorResult*>(
+     reinterpret_cast<TErrorResult<AssertAndSuppressCleanupPolicy>*>(this));
+}
+
+// A class for use when an ErrorResult should just automatically be ignored.
+// This doesn't inherit from ErrorResult so we don't make two separate calls to
+// SuppressException.
+class IgnoredErrorResult :
+    public binding_danger::TErrorResult<binding_danger::JustSuppressCleanupPolicy>
+{
 };
 
 /******************************************************************************
  ** Macros for checking results
  ******************************************************************************/
 
 #define ENSURE_SUCCESS(res, ret)                                          \
   do {                                                                    \
--- a/dom/bindings/test/test_promise_rejections_from_jsimplemented.html
+++ b/dom/bindings/test/test_promise_rejections_from_jsimplemented.html
@@ -20,105 +20,114 @@ https://bugzilla.mozilla.org/show_bug.cg
     is(exn.name, name,
        "Should have the right exception name in test " + testNumber);
     is("filename" in exn ? exn.filename : exn.fileName, filename,
        "Should have the right file name in test " + testNumber);
     is(exn.message, message,
        "Should have the right message in test " + testNumber);
     is(exn.code, code, "Should have the right .code in test " + testNumber);
     if (message === "") {
-      is(exn.name, "NS_ERROR_UNEXPECTED",
+      is(exn.name, "InternalError",
          "Should have one of our synthetic exceptions in test " + testNumber);
     }
     is(exn.stack, stack, "Should have the right stack in test " + testNumber);
   }
 
   function ensurePromiseFail(testNumber, value) {
     ok(false, "Test " + testNumber + " should not have a fulfilled promise");
   }
 
   function doTest() {
     var t = new TestInterfaceJS();
     /* Async parent frames from pushPrefEnv don't show up in e10s.  */
     var isE10S = !SpecialPowers.isMainProcess();
     var asyncStack = SpecialPowers.getBoolPref("javascript.options.asyncstack");
     var ourFile = location.href;
-    var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:3
+    var unwrapError = "Promise rejection value is a non-unwrappable cross-compartment wrapper.";
+    var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:130:3
 ` : "";
 
     Promise.all([
       t.testPromiseWithThrowingChromePromiseInit().then(
           ensurePromiseFail.bind(null, 1),
-          checkExn.bind(null, 48, "NS_ERROR_UNEXPECTED", "", undefined,
-                        ourFile, 1,
-                        `doTest@${ourFile}:48:7
+          checkExn.bind(null, 49, "InternalError", unwrapError,
+                        undefined, ourFile, 1,
+                        `doTest@${ourFile}:49:7
 ` +
                         parentFrame)),
       t.testPromiseWithThrowingContentPromiseInit(function() {
           thereIsNoSuchContentFunction1();
         }).then(
           ensurePromiseFail.bind(null, 2),
-          checkExn.bind(null, 56, "ReferenceError",
+          checkExn.bind(null, 57, "ReferenceError",
                         "thereIsNoSuchContentFunction1 is not defined",
                         undefined, ourFile, 2,
-                        `doTest/<@${ourFile}:56:11
-doTest@${ourFile}:55:7
+                        `doTest/<@${ourFile}:57:11
+doTest@${ourFile}:56:7
 ` +
                         parentFrame)),
       t.testPromiseWithThrowingChromeThenFunction().then(
           ensurePromiseFail.bind(null, 3),
-          checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 3, "")),
+          checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 3, asyncStack ? (`Async*doTest@${ourFile}:67:7
+` +
+                        parentFrame) : "")),
       t.testPromiseWithThrowingContentThenFunction(function() {
           thereIsNoSuchContentFunction2();
         }).then(
           ensurePromiseFail.bind(null, 4),
-          checkExn.bind(null, 70, "ReferenceError",
+          checkExn.bind(null, 73, "ReferenceError",
                         "thereIsNoSuchContentFunction2 is not defined",
                         undefined, ourFile, 4,
-                        `doTest/<@${ourFile}:70:11
+                        `doTest/<@${ourFile}:73:11
 ` +
-                        (asyncStack ? `Async*doTest@${ourFile}:69:7
+                        (asyncStack ? `Async*doTest@${ourFile}:72:7
 ` : "") +
                         parentFrame)),
       t.testPromiseWithThrowingChromeThenable().then(
           ensurePromiseFail.bind(null, 5),
-          checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 5, "")),
+          checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 5, asyncStack ? (`Async*doTest@${ourFile}:84:7
+` +
+                        parentFrame) : "")),
       t.testPromiseWithThrowingContentThenable({
             then: function() { thereIsNoSuchContentFunction3(); }
         }).then(
           ensurePromiseFail.bind(null, 6),
-          checkExn.bind(null, 85, "ReferenceError",
+          checkExn.bind(null, 90, "ReferenceError",
                         "thereIsNoSuchContentFunction3 is not defined",
                         undefined, ourFile, 6,
-                        `doTest/<.then@${ourFile}:85:32
-`)),
+                        `doTest/<.then@${ourFile}:90:32
+` + (asyncStack ? `Async*doTest@${ourFile}:89:7\n` + parentFrame : ""))),
       t.testPromiseWithDOMExceptionThrowingPromiseInit().then(
           ensurePromiseFail.bind(null, 7),
-          checkExn.bind(null, 93, "NotFoundError",
+          checkExn.bind(null, 98, "NotFoundError",
                         "We are a second DOMException",
                         DOMException.NOT_FOUND_ERR, ourFile, 7,
-                        `doTest@${ourFile}:93:7
+                        `doTest@${ourFile}:98:7
 ` +
                         parentFrame)),
       t.testPromiseWithDOMExceptionThrowingThenFunction().then(
           ensurePromiseFail.bind(null, 8),
-          checkExn.bind(null, asyncStack ? 101 : 0, "NetworkError",
+          checkExn.bind(null, asyncStack ? 106 : 0, "NetworkError",
                          "We are a third DOMException",
                         DOMException.NETWORK_ERR, asyncStack ? ourFile : "", 8,
-                        (asyncStack ? `Async*doTest@${ourFile}:101:7
+                        (asyncStack ? `Async*doTest@${ourFile}:106:7
 ` +
                          parentFrame : ""))),
       t.testPromiseWithDOMExceptionThrowingThenable().then(
           ensurePromiseFail.bind(null, 9),
-          checkExn.bind(null, 0, "TypeMismatchError",
+          checkExn.bind(null, asyncStack ? 114 : 0, "TypeMismatchError",
                         "We are a fourth DOMException",
-                         DOMException.TYPE_MISMATCH_ERR, "", 9, "")),
+                        DOMException.TYPE_MISMATCH_ERR,
+                        asyncStack ? ourFile : "", 9,
+                        (asyncStack ? `Async*doTest@${ourFile}:114:7
+` +
+                         parentFrame : ""))),
     ]).then(SimpleTest.finish,
-            function() {
-              ok(false, "One of our catch statements totally failed");
+            function(err) {
+              ok(false, "One of our catch statements totally failed with err" + err + ', stack: ' + (err ? err.stack : ''));
               SimpleTest.finish();
             });
   }
 
   SpecialPowers.pushPrefEnv({set: [['dom.expose_test_interfaces', true]]},
                             doTest);
   </script>
 </head>
--- a/dom/filesystem/compat/tests/test_no_dnd.html
+++ b/dom/filesystem/compat/tests/test_no_dnd.html
@@ -2,49 +2,50 @@
 <html>
 <head>
   <title>Test for Blink FileSystem API - no DND == no webkitEntries</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 
 <body>
-<input id="entries" type="file"></input>
 <script type="application/javascript;version=1.7">
 
 var fileEntry;
 var directoryEntry;
 var script;
+var entries;
 
 function setup_tests() {
   SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
                                      ["dom.webkitBlink.filesystem.enabled", true]]}, next);
 }
 
 function populate_entries() {
+  entries = document.createElement('input');
+  entries.setAttribute('type', 'file');
+  document.body.appendChild(entries);
+
   var url = SimpleTest.getTestFileURL("script_entries.js");
   script = SpecialPowers.loadChromeScript(url);
 
   function onOpened(message) {
-    var entries = document.getElementById('entries');
-
     for (var i = 0 ; i < message.data.length; ++i) {
       if (message.data[i] instanceof File) {
         SpecialPowers.wrap(entries).mozSetFileArray([message.data[i]]);
         next();
       }
     }
   }
 
   script.addMessageListener("entries.opened", onOpened);
   script.sendAsyncMessage("entries.open");
 }
 
 function test_entries() {
-  var entries = document.getElementById('entries');
   ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries");
   is(entries.webkitEntries.length, 0, "HTMLInputElement.webkitEntries.length == 0");
   is(entries.files.length, 1, "HTMLInputElement.files is still populated");
 
   next();
 }
 
 function cleanUpTestingFiles() {
--- a/dom/geolocation/nsGeolocation.cpp
+++ b/dom/geolocation/nsGeolocation.cpp
@@ -558,28 +558,20 @@ nsGeolocationRequest::GetRequester(nsICo
   return NS_OK;
 }
 
 void
 nsGeolocationRequest::SetTimeoutTimer()
 {
   StopTimeoutTimer();
 
-  int32_t timeout;
-  if (mOptions && (timeout = mOptions->mTimeout) != 0) {
-
-    if (timeout < 0) {
-      timeout = 0;
-    } else if (timeout < 10) {
-      timeout = 10;
-    }
-
+  if (mOptions && mOptions->mTimeout != 0 && mOptions->mTimeout != 0x7fffffff) {
     mTimeoutTimer = do_CreateInstance("@mozilla.org/timer;1");
     RefPtr<TimerCallbackHolder> holder = new TimerCallbackHolder(this);
-    mTimeoutTimer->InitWithCallback(holder, timeout, nsITimer::TYPE_ONE_SHOT);
+    mTimeoutTimer->InitWithCallback(holder, mOptions->mTimeout, nsITimer::TYPE_ONE_SHOT);
   }
 }
 
 void
 nsGeolocationRequest::StopTimeoutTimer()
 {
   if (mTimeoutTimer) {
     mTimeoutTimer->Cancel();
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -3407,17 +3407,18 @@ HTMLInputElement::Focus(ErrorResult& aEr
 }
 
 #if defined(XP_WIN) || defined(XP_LINUX)
 bool
 HTMLInputElement::IsNodeApzAwareInternal() const
 {
   // Tell APZC we may handle mouse wheel event and do preventDefault when input
   // type is number.
-  return (mType == NS_FORM_INPUT_NUMBER) || nsINode::IsNodeApzAwareInternal();
+  return (mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE) ||
+         nsINode::IsNodeApzAwareInternal();
 }
 #endif
 
 bool
 HTMLInputElement::IsInteractiveHTMLContent(bool aIgnoreTabindex) const
 {
   return mType != NS_FORM_INPUT_HIDDEN ||
          nsGenericHTMLFormElementWithState::IsInteractiveHTMLContent(aIgnoreTabindex);
@@ -4502,27 +4503,40 @@ HTMLInputElement::PostHandleEvent(EventC
               StopNumberControlSpinnerSpin();
             }
           }
           break;
         }
 #if defined(XP_WIN) || defined(XP_LINUX)
         case eWheel: {
           // Handle wheel events as increasing / decreasing the input element's
-          // value when it's focused and it's type is number.
+          // value when it's focused and it's type is number or range.
           WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent();
           if (!aVisitor.mEvent->DefaultPrevented() &&
               aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent &&
               wheelEvent->mDeltaY != 0 &&
-              wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL &&
-              mType == NS_FORM_INPUT_NUMBER) {
-            nsNumberControlFrame* numberControlFrame =
-              do_QueryFrame(GetPrimaryFrame());
-            if (numberControlFrame && numberControlFrame->IsFocused()) {
-              StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+              wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL) {
+            if (mType == NS_FORM_INPUT_NUMBER) {
+              nsNumberControlFrame* numberControlFrame =
+                do_QueryFrame(GetPrimaryFrame());
+              if (numberControlFrame && numberControlFrame->IsFocused()) {
+                StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+                aVisitor.mEvent->PreventDefault();
+              }
+            } else if (mType == NS_FORM_INPUT_RANGE &&
+                       nsContentUtils::IsFocusedContent(this) &&
+                       GetMinimum() < GetMaximum()) {
+              Decimal value = GetValueAsDecimal();
+              Decimal step = GetStep();
+              if (step == kStepAny) {
+                step = GetDefaultStep();
+              }
+              MOZ_ASSERT(value.isFinite() && step.isFinite());
+              SetValueOfRangeForUserEvent(wheelEvent->mDeltaY < 0 ?
+                                          value + step : value - step);
               aVisitor.mEvent->PreventDefault();
             }
           }
           break;
         }
 #endif
         default:
           break;
@@ -6009,17 +6023,17 @@ FireEventForAccessibility(nsIDOMHTMLInpu
   return NS_OK;
 }
 #endif
 
 void
 HTMLInputElement::UpdateApzAwareFlag()
 {
 #if defined(XP_WIN) || defined(XP_LINUX)
-  if (mType == NS_FORM_INPUT_NUMBER) {
+  if ((mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE)) {
     SetMayBeApzAware();
   }
 #endif
 }
 
 nsresult
 HTMLInputElement::SetDefaultValueAsValue()
 {
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -615,10 +615,14 @@ skip-if = buildapp == 'b2g' # bug 112901
 [test_bug1230665.html]
 [test_filepicker_default_directory.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android'
 [test_bug1233598.html]
 [test_bug1250401.html]
 [test_bug1260664.html]
 [test_bug1261673.html]
 skip-if = (os != 'win' && os != 'linux')
+[test_bug1261674-1.html]
+skip-if = (os != 'win' && os != 'linux')
+[test_bug1261674-2.html]
+skip-if = (os != 'win' && os != 'linux')
 [test_bug1260704.html]
 [test_allowMedia.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/test_bug1261674-1.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1261674</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" value=5 max=10 min=0>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+  let input = window.document.getElementById("test_input");
+
+  // focus: whether the target input element is focused
+  // deltaY: deltaY of WheelEvent
+  // deltaMode: deltaMode of WheelEvent
+  // valueChanged: expected value changes after input element handled the wheel event
+  let params = [
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1},
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1},
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+    {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0},
+    {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}
+  ];
+
+  let testIdx = 0;
+  let result = parseInt(input.value);
+
+  function runNext() {
+    let p = params[testIdx];
+    (p["focus"]) ? input.focus() : input.blur();
+    result += parseInt(p["valueChanged"]);
+    sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => {
+      ok(input.value == result,
+         "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+      (++testIdx >= params.length) ? SimpleTest.finish() : runNext();
+    });
+  }
+
+  input.addEventListener("input", () => {
+    ok(input.value == result,
+       "Test-" + testIdx + " receive input event, expect " + result + " get " + input.value);
+  }, false);
+
+  runNext();
+}
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/html/test/test_bug1261674-2.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1261674</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" max=0 min=10>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+  let input = window.document.getElementById("test_input");
+
+  // deltaY: deltaY of WheelEvent
+  // deltaMode: deltaMode of WheelEvent
+  let params = [
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}
+  ];
+
+  let testIdx = 0;
+  let result = parseInt(input.value);
+
+  function runNext() {
+    let p = params[testIdx];
+    (p["focus"]) ? input.focus() : input.blur();
+    sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => {
+      ok(input.value == result,
+         "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+      testIdx++;
+      (testIdx >= params.length) ? SimpleTest.finish() : runNext();
+    });
+  }
+
+  input.addEventListener("input", () => {
+    ok(false, "Wheel event should be no effect to range input element with max < min");
+  }, false);
+
+  runNext();
+}
+</script>
+</body>
+</html>
--- a/dom/presentation/PresentationConnection.cpp
+++ b/dom/presentation/PresentationConnection.cpp
@@ -183,19 +183,18 @@ PresentationConnection::Close(ErrorResul
     service->CloseSession(mId,
                           mRole,
                           nsIPresentationService::CLOSED_REASON_CLOSED)));
 }
 
 void
 PresentationConnection::Terminate(ErrorResult& aRv)
 {
-  // It only works when the state is CONNECTED or CONNECTING.
-  if (NS_WARN_IF(mState != PresentationConnectionState::Connected &&
-                 mState != PresentationConnectionState::Connecting)) {
+  // It only works when the state is CONNECTED.
+  if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
     return;
   }
 
   nsCOMPtr<nsIPresentationService> service =
     do_GetService(PRESENTATION_SERVICE_CONTRACTID);
   if(NS_WARN_IF(!service)) {
     aRv.Throw(NS_ERROR_DOM_OPERATION_ERR);
     return;
@@ -277,31 +276,32 @@ PresentationConnection::ProcessStateChan
         CopyUTF8toUTF16(message, errorMsg);
       }
 
       NS_WARN_IF(NS_FAILED(DispatchConnectionClosedEvent(reason, errorMsg)));
 
       return RemoveFromLoadGroup();
     }
     case PresentationConnectionState::Terminated: {
+      // Ensure onterminate event is fired.
+      RefPtr<AsyncEventDispatcher> asyncDispatcher =
+        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
+      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
+
       nsCOMPtr<nsIPresentationService> service =
         do_GetService(PRESENTATION_SERVICE_CONTRACTID);
       if (NS_WARN_IF(!service)) {
         return NS_ERROR_NOT_AVAILABLE;
       }
 
       nsresult rv = service->UnregisterSessionListener(mId, mRole);
       if(NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
-      RefPtr<AsyncEventDispatcher> asyncDispatcher =
-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
-      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
-
       return RemoveFromLoadGroup();
     }
     default:
       MOZ_CRASH("Unknown presentation session state.");
       return NS_ERROR_INVALID_ARG;
   }
 }
 
--- a/dom/presentation/PresentationDeviceManager.cpp
+++ b/dom/presentation/PresentationDeviceManager.cpp
@@ -9,16 +9,17 @@
 #include "mozilla/Services.h"
 #include "MainThreadUtils.h"
 #include "nsCategoryCache.h"
 #include "nsCOMPtr.h"
 #include "nsIMutableArray.h"
 #include "nsIObserverService.h"
 #include "nsXULAppAPI.h"
 #include "PresentationSessionRequest.h"
+#include "PresentationTerminateRequest.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_ISUPPORTS(PresentationDeviceManager,
                   nsIPresentationDeviceManager,
                   nsIPresentationDeviceListener,
                   nsIObserver,
@@ -234,16 +235,38 @@ PresentationDeviceManager::OnSessionRequ
     new PresentationSessionRequest(aDevice, aUrl, aPresentationId, aControlChannel);
   obs->NotifyObservers(request,
                        PRESENTATION_SESSION_REQUEST_TOPIC,
                        nullptr);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+PresentationDeviceManager::OnTerminateRequest(nsIPresentationDevice* aDevice,
+                                              const nsAString& aPresentationId,
+                                              nsIPresentationControlChannel* aControlChannel,
+                                              bool aIsFromReceiver)
+{
+  NS_ENSURE_ARG(aDevice);
+  NS_ENSURE_ARG(aControlChannel);
+
+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+  NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
+
+  RefPtr<PresentationTerminateRequest> request =
+    new PresentationTerminateRequest(aDevice, aPresentationId,
+                                     aControlChannel, aIsFromReceiver);
+  obs->NotifyObservers(request,
+                       PRESENTATION_TERMINATE_REQUEST_TOPIC,
+                       nullptr);
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 PresentationDeviceManager::Observe(nsISupports *aSubject,
                                    const char *aTopic,
                                    const char16_t *aData)
 {
   if (!strcmp(aTopic, "profile-after-change")) {
     Init();
--- a/dom/presentation/PresentationService.cpp
+++ b/dom/presentation/PresentationService.cpp
@@ -1,38 +1,66 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
+#include "PresentationService.h"
+
 #include "ipc/PresentationIPCService.h"
 #include "mozilla/Services.h"
 #include "mozIApplication.h"
+#include "nsGlobalWindow.h"
 #include "nsIAppsService.h"
 #include "nsIObserverService.h"
 #include "nsIPresentationControlChannel.h"
 #include "nsIPresentationDeviceManager.h"
 #include "nsIPresentationDevicePrompt.h"
 #include "nsIPresentationListener.h"
 #include "nsIPresentationRequestUIGlue.h"
 #include "nsIPresentationSessionRequest.h"
+#include "nsIPresentationTerminateRequest.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
 #include "PresentationLog.h"
-#include "PresentationService.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 namespace mozilla {
 namespace dom {
 
+static bool
+IsSameDevice(nsIPresentationDevice* aDevice, nsIPresentationDevice* aDeviceAnother) {
+  if (!aDevice || !aDeviceAnother) {
+    return false;
+  }
+
+  nsAutoCString deviceId;
+  aDevice->GetId(deviceId);
+  nsAutoCString anotherId;
+  aDeviceAnother->GetId(anotherId);
+  if (!deviceId.Equals(anotherId)) {
+    return false;
+  }
+
+  nsAutoCString deviceType;
+  aDevice->GetType(deviceType);
+  nsAutoCString anotherType;
+  aDeviceAnother->GetType(anotherType);
+  if (!deviceType.Equals(anotherType)) {
+    return false;
+  }
+
+  return true;
+}
+
 /*
  * Implementation of PresentationDeviceRequest
  */
 
 class PresentationDeviceRequest final : public nsIPresentationDeviceRequest
 {
 public:
   NS_DECL_ISUPPORTS
@@ -186,16 +214,20 @@ PresentationService::Init()
   rv = obs->AddObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC, false);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return false;
   }
   rv = obs->AddObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC, false);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return false;
   }
+  rv = obs->AddObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC, false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return false;
+  }
 
   nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
     do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
   if (NS_WARN_IF(!deviceManager)) {
     return false;
   }
 
   rv = deviceManager->GetDeviceAvailable(&mIsAvailable);
@@ -214,16 +246,23 @@ PresentationService::Observe(nsISupports
     return HandleDeviceChange();
   } else if (!strcmp(aTopic, PRESENTATION_SESSION_REQUEST_TOPIC)) {
     nsCOMPtr<nsIPresentationSessionRequest> request(do_QueryInterface(aSubject));
     if (NS_WARN_IF(!request)) {
       return NS_ERROR_FAILURE;
     }
 
     return HandleSessionRequest(request);
+  } else if (!strcmp(aTopic, PRESENTATION_TERMINATE_REQUEST_TOPIC)) {
+    nsCOMPtr<nsIPresentationTerminateRequest> request(do_QueryInterface(aSubject));
+    if (NS_WARN_IF(!request)) {
+      return NS_ERROR_FAILURE;
+    }
+
+    return HandleTerminateRequest(request);
   } else if (!strcmp(aTopic, "profile-after-change")) {
     // It's expected since we add and entry to |kLayoutCategories| in
     // |nsLayoutModule.cpp| to launch this service earlier.
     return NS_OK;
   }
 
   MOZ_ASSERT(false, "Unexpected topic for PresentationService");
   return NS_ERROR_UNEXPECTED;
@@ -240,16 +279,17 @@ PresentationService::HandleShutdown()
   mSessionInfoAtController.Clear();
   mSessionInfoAtReceiver.Clear();
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
     obs->RemoveObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC);
     obs->RemoveObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC);
+    obs->RemoveObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC);
   }
 }
 
 nsresult
 PresentationService::HandleDeviceChange()
 {
   nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
     do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
@@ -355,16 +395,68 @@ PresentationService::HandleSessionReques
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
   nsCOMPtr<Promise> realPromise = do_QueryInterface(promise);
   static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(realPromise);
 
   return NS_OK;
 }
 
+nsresult
+PresentationService::HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest)
+{
+  nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+  nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel));
+  if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) {
+    return rv;
+  }
+
+  nsAutoString sessionId;
+  rv = aRequest->GetPresentationId(sessionId);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  nsCOMPtr<nsIPresentationDevice> device;
+  rv = aRequest->GetDevice(getter_AddRefs(device));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  bool isFromReceiver;
+  rv = aRequest->GetIsFromReceiver(&isFromReceiver);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  RefPtr<PresentationSessionInfo> info;
+  if (!isFromReceiver) {
+    info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
+  } else {
+    info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_CONTROLLER);
+  }
+  if (NS_WARN_IF(!info)) {
+    // Cannot terminate non-existed session.
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+    return NS_ERROR_DOM_ABORT_ERR;
+  }
+
+  // Check if terminate request comes from known device.
+  RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
+  if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+    return NS_ERROR_DOM_ABORT_ERR;
+  }
+
+  return info->OnTerminate(ctrlChannel);
+}
+
 void
 PresentationService::NotifyAvailableChange(bool aIsAvailable)
 {
   nsTObserverArray<nsCOMPtr<nsIPresentationAvailabilityListener>>::ForwardIterator iter(mAvailabilityListeners);
   while (iter.HasMore()) {
     nsCOMPtr<nsIPresentationAvailabilityListener> listener = iter.GetNext();
     NS_WARN_IF(NS_FAILED(listener->NotifyAvailableChange(aIsAvailable)));
   }
@@ -520,17 +612,16 @@ PresentationService::CloseSession(const 
              aRole == nsIPresentationService::ROLE_RECEIVER);
 
   RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
   if (NS_WARN_IF(!info)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (aClosedReason == nsIPresentationService::CLOSED_REASON_WENTAWAY) {
-    UntrackSessionInfo(aSessionId, aRole);
     // Remove nsIPresentationSessionListener since we don't want to dispatch
     // PresentationConnectionClosedEvent if the page is went away.
     info->SetListener(nullptr);
   }
 
   return info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED);
 }
 
@@ -606,18 +697,19 @@ PresentationService::UnregisterSessionLi
                                                uint8_t aRole)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
              aRole == nsIPresentationService::ROLE_RECEIVER);
 
   RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
   if (info) {
-    NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_TERMINATED)));
-    UntrackSessionInfo(aSessionId, aRole);
+    // When content side decide not handling this session anymore, simply
+    // close the connection. Session info is kept for reconnection.
+    NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED)));
     return info->SetListener(nullptr);
   }
   return NS_OK;
 }
 
 nsresult
 PresentationService::RegisterTransportBuilder(const nsAString& aSessionId,
                                               uint8_t aRole,
@@ -724,16 +816,27 @@ PresentationService::UntrackSessionInfo(
                                         uint8_t aRole)
 {
   MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
              aRole == nsIPresentationService::ROLE_RECEIVER);
   // Remove the session info.
   if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
     mSessionInfoAtController.Remove(aSessionId);
   } else {
+    // Terminate receiver page.
+    uint64_t windowId;
+    nsresult rv = GetWindowIdBySessionIdInternal(aSessionId, &windowId);
+    if (NS_SUCCEEDED(rv)) {
+      NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void {
+        if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) {
+          window->Close();
+        }
+      }));
+    }
+
     mSessionInfoAtReceiver.Remove(aSessionId);
   }
 
   // Remove the in-process responding info if there's still any.
   RemoveRespondingSessionId(aSessionId);
 
   return NS_OK;
 }
--- a/dom/presentation/PresentationService.h
+++ b/dom/presentation/PresentationService.h
@@ -9,16 +9,17 @@
 
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsTObserverArray.h"
 #include "PresentationServiceBase.h"
 #include "PresentationSessionInfo.h"
 
 class nsIPresentationSessionRequest;
+class nsIPresentationTerminateRequest;
 class nsIURI;
 class nsIPresentationSessionTransportBuilder;
 
 namespace mozilla {
 namespace dom {
 
 class PresentationDeviceRequest;
 class PresentationRespondingInfo;
@@ -61,16 +62,17 @@ public:
 
 private:
   friend class PresentationDeviceRequest;
 
   virtual ~PresentationService();
   void HandleShutdown();
   nsresult HandleDeviceChange();
   nsresult HandleSessionRequest(nsIPresentationSessionRequest* aRequest);
+  nsresult HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest);
   void NotifyAvailableChange(bool aIsAvailable);
   bool IsAppInstalled(nsIURI* aUri);
 
   // This is meant to be called by PresentationDeviceRequest.
   already_AddRefed<PresentationSessionInfo>
   CreateControllingSessionInfo(const nsAString& aUrl,
                                const nsAString& aSessionId,
                                uint64_t aWindowId);
--- a/dom/presentation/PresentationSessionInfo.cpp
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -231,16 +231,17 @@ PresentationSessionInfo::Shutdown(nsresu
 
   // Close the data transport channel if any.
   if (mTransport) {
     // |mIsTransportReady| will be unset once |NotifyTransportClosed| is called.
     NS_WARN_IF(NS_FAILED(mTransport->Close(aReason)));
   }
 
   mIsResponderReady = false;
+  mIsOnTerminating = false;
 
   SetBuilder(nullptr);
 }
 
 nsresult
 PresentationSessionInfo::SetListener(nsIPresentationSessionListener* aListener)
 {
   mListener = aListener;
@@ -279,19 +280,52 @@ PresentationSessionInfo::Send(const nsAS
 nsresult
 PresentationSessionInfo::Close(nsresult aReason,
                                uint32_t aState)
 {
   if (NS_WARN_IF(!IsSessionReady())) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
+  // Do nothing if session is already terminated.
+  if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+    return NS_OK;
+  }
+
   SetStateWithReason(aState, aReason);
 
-  Shutdown(aReason);
+  switch (aState) {
+    case nsIPresentationSessionListener::STATE_CLOSED: {
+      Shutdown(aReason);
+      break;
+    }
+    case nsIPresentationSessionListener::STATE_TERMINATED: {
+      if (!mControlChannel) {
+        nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+        nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
+        if (NS_SUCCEEDED(rv)) {
+          SetControlChannel(ctrlChannel);
+        }
+        return rv;
+      }
+
+      return mControlChannel->Terminate(mSessionId);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PresentationSessionInfo::OnTerminate(nsIPresentationControlChannel* aControlChannel)
+{
+  mIsOnTerminating = true; // Mark for terminating transport channel
+  SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED, NS_OK);
+  SetControlChannel(aControlChannel);
+
   return NS_OK;
 }
 
 nsresult
 PresentationSessionInfo::ReplySuccess()
 {
   SetStateWithReason(nsIPresentationSessionListener::STATE_CONNECTED, NS_OK);
   return NS_OK;
@@ -337,16 +371,28 @@ PresentationSessionInfo::GetWindow()
 
 /* virtual */ bool
 PresentationSessionInfo::IsAccessible(base::ProcessId aProcessId)
 {
   // No restriction by default.
   return true;
 }
 
+void
+PresentationSessionInfo::ContinueTermination()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mControlChannel);
+
+  if (NS_WARN_IF(NS_FAILED(mControlChannel->Terminate(mSessionId)))
+      || mIsOnTerminating) {
+    Shutdown(NS_OK);
+  }
+}
+
 // nsIPresentationSessionTransportCallback
 NS_IMETHODIMP
 PresentationSessionInfo::NotifyTransportReady()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   mIsTransportReady = true;
 
@@ -687,16 +733,37 @@ PresentationControllingInfo::OnAnswer(ns
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationControllingInfo::NotifyConnected()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  switch (mState) {
+    case nsIPresentationSessionListener::STATE_CONNECTING: {
+      Unused << NS_WARN_IF(NS_FAILED(BuildTransport()));
+      break;
+    }
+    case nsIPresentationSessionListener::STATE_TERMINATED: {
+      ContinueTermination();
+      break;
+    }
+    default:
+      break;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PresentationControllingInfo::BuildTransport()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
   nsresult rv = mControlChannel->Launch(GetSessionId(), GetUrl());
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   if (!Preferences::GetBool("dom.presentation.session_transport.data_channel.enable")) {
     // Build TCP session transport
     return GetAddress();
@@ -1134,17 +1201,20 @@ PresentationPresentingInfo::OnIceCandida
     builder = do_QueryInterface(mBuilder);
 
   return builder->OnIceCandidate(aCandidate);
 }
 
 NS_IMETHODIMP
 PresentationPresentingInfo::NotifyConnected()
 {
-  // Do nothing.
+  if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+    ContinueTermination();
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationPresentingInfo::NotifyDisconnected(nsresult aReason)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
--- a/dom/presentation/PresentationSessionInfo.h
+++ b/dom/presentation/PresentationSessionInfo.h
@@ -100,16 +100,18 @@ public:
     }
   }
 
   nsresult Send(const nsAString& aData);
 
   nsresult Close(nsresult aReason,
                  uint32_t aState);
 
+  nsresult OnTerminate(nsIPresentationControlChannel* aControlChannel);
+
   nsresult ReplyError(nsresult aReason);
 
   virtual bool IsAccessible(base::ProcessId aProcessId);
 
 protected:
   virtual ~PresentationSessionInfo()
   {
     Shutdown(NS_OK);
@@ -137,28 +139,31 @@ protected:
 
     // Notify session state change.
     if (mListener) {
       nsresult rv = mListener->NotifyStateChange(mSessionId, mState, aReason);
       NS_WARN_IF(NS_FAILED(rv));
     }
   }
 
+  void ContinueTermination();
+
   // Should be nsIPresentationChannelDescription::TYPE_TCP/TYPE_DATACHANNEL
   uint8_t mTransportType = 0;
 
   nsPIDOMWindowInner* GetWindow();
 
   nsString mUrl;
   nsString mSessionId;
   // mRole should be nsIPresentationService::ROLE_CONTROLLER
   //              or nsIPresentationService::ROLE_RECEIVER.
   uint8_t mRole;
   bool mIsResponderReady;
   bool mIsTransportReady;
+  bool mIsOnTerminating = false;
   uint32_t mState; // CONNECTED, CLOSED, TERMINATED
   nsresult mReason;
   nsCOMPtr<nsIPresentationSessionListener> mListener;
   nsCOMPtr<nsIPresentationDevice> mDevice;
   nsCOMPtr<nsIPresentationSessionTransport> mTransport;
   nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
   nsCOMPtr<nsIPresentationSessionTransportBuilder> mBuilder;
 };
@@ -188,16 +193,18 @@ private:
   }
 
   void Shutdown(nsresult aReason) override;
 
   nsresult GetAddress();
 
   nsresult OnGetAddress(const nsACString& aAddress);
 
+  nsresult BuildTransport();
+
   nsCOMPtr<nsIServerSocket> mServerSocket;
 };
 
 // Session info with presenting browsing context (receiver side) behaviors.
 class PresentationPresentingInfo final : public PresentationSessionInfo
                                        , public PromiseNativeHandler
                                        , public nsITimerCallback
 {
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "PresentationTerminateRequest.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationDevice.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationTerminateRequest, nsIPresentationTerminateRequest)
+
+PresentationTerminateRequest::PresentationTerminateRequest(
+                                 nsIPresentationDevice* aDevice,
+                                 const nsAString& aPresentationId,
+                                 nsIPresentationControlChannel* aControlChannel,
+                                 bool aIsFromReceiver)
+  : mPresentationId(aPresentationId)
+  , mDevice(aDevice)
+  , mControlChannel(aControlChannel)
+  , mIsFromReceiver(aIsFromReceiver)
+{
+}
+
+PresentationTerminateRequest::~PresentationTerminateRequest()
+{
+}
+
+// nsIPresentationTerminateRequest
+NS_IMETHODIMP
+PresentationTerminateRequest::GetDevice(nsIPresentationDevice** aRetVal)
+{
+  NS_ENSURE_ARG_POINTER(aRetVal);
+
+  nsCOMPtr<nsIPresentationDevice> device = mDevice;
+  device.forget(aRetVal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetPresentationId(nsAString& aRetVal)
+{
+  aRetVal = mPresentationId;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetControlChannel(
+                                        nsIPresentationControlChannel** aRetVal)
+{
+  NS_ENSURE_ARG_POINTER(aRetVal);
+
+  nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel;
+  controlChannel.forget(aRetVal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetIsFromReceiver(bool* aRetVal)
+{
+  *aRetVal = mIsFromReceiver;
+
+  return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PresentationTerminateRequest_h__
+#define mozilla_dom_PresentationTerminateRequest_h__
+
+#include "nsIPresentationTerminateRequest.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationTerminateRequest final : public nsIPresentationTerminateRequest
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIPRESENTATIONTERMINATEREQUEST
+
+  PresentationTerminateRequest(nsIPresentationDevice* aDevice,
+                               const nsAString& aPresentationId,
+                               nsIPresentationControlChannel* aControlChannel,
+                               bool aIsFromReceiver);
+
+private:
+  virtual ~PresentationTerminateRequest();
+
+  nsString mPresentationId;
+  nsCOMPtr<nsIPresentationDevice> mDevice;
+  nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
+  bool mIsFromReceiver;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_PresentationTerminateRequest_h__ */
+
--- a/dom/presentation/interfaces/moz.build
+++ b/dom/presentation/interfaces/moz.build
@@ -13,16 +13,17 @@ XPIDL_SOURCES += [
     'nsIPresentationDeviceProvider.idl',
     'nsIPresentationListener.idl',
     'nsIPresentationLocalDevice.idl',
     'nsIPresentationRequestUIGlue.idl',
     'nsIPresentationService.idl',
     'nsIPresentationSessionRequest.idl',
     'nsIPresentationSessionTransport.idl',
     'nsIPresentationSessionTransportBuilder.idl',
+    'nsIPresentationTerminateRequest.idl',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     XPIDL_SOURCES += [
         'nsIPresentationNetworkHelper.idl',
     ]
 
 XPIDL_MODULE = 'dom_presentation'
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -106,13 +106,20 @@ interface nsIPresentationControlChannel:
    * Launch a presentation on remote endpoint.
    * @param presentationId The Id for representing this session.
    * @param url The URL requested to open by remote device.
    * @throws NS_ERROR_FAILURE on failure
    */
   void launch(in DOMString presentationId, in DOMString url);
 
   /*
+   * Terminate a presentation on remote endpoint.
+   * @param presentationId The Id for representing this session.
+   * @throws NS_ERROR_FAILURE on failure
+   */
+  void terminate(in DOMString presentationId);
+
+  /*
    * Disconnect the control channel.
    * @param reason The reason of disconnecting channel; NS_OK represents normal.
    */
   void disconnect(in nsresult reason);
 };
--- a/dom/presentation/interfaces/nsIPresentationControlService.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlService.idl
@@ -40,16 +40,28 @@ interface nsIPresentationControlServerLi
    * @param aUrl The URL requested to open by remote device.
    * @param aPresentationId The Id for representing this session.
    * @param aControlChannel The control channel for this session.
    */
   void onSessionRequest(in nsITCPDeviceInfo aDeviceInfo,
                         in DOMString aUrl,
                         in DOMString aPresentationId,
                         in nsIPresentationControlChannel aControlChannel);
+
+  /**
+   * Callback while the remote host is requesting to terminate a presentation session.
+   * @param aDeviceInfo The device information related to the remote host.
+   * @param aPresentationId The Id for representing this session.
+   * @param aControlChannel The control channel for this session.
+   * @param aIsFromReceiver true if termination is initiated by receiver.
+   */
+  void onTerminateRequest(in nsITCPDeviceInfo aDeviceInfo,
+                          in DOMString aPresentationId,
+                          in nsIPresentationControlChannel aControlChannel,
+                          in boolean aIsFromReceiver);
 };
 
 /**
  * Presentation control service which can be used for both presentation
  * control client and server.
  */
 [scriptable, uuid(55d6b605-2389-4aae-a8fe-60d4440540ea)]
 interface nsIPresentationControlService: nsISupports
--- a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
+++ b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
@@ -27,16 +27,28 @@ interface nsIPresentationDeviceListener:
    * @param url The URL requested to open by remote device.
    * @param presentationId The Id for representing this session.
    * @param controlChannel The control channel for this session.
    */
   void onSessionRequest(in nsIPresentationDevice device,
                         in DOMString url,
                         in DOMString presentationId,
                         in nsIPresentationControlChannel controlChannel);
+
+  /*
+   * Callback while the remote device is requesting to terminate a presentation session.
+   * @param device The remote device that sent session request.
+   * @param presentationId The Id for representing this session.
+   * @param controlChannel The control channel for this session.
+   * @param aIsFromReceiver true if termination is initiated by receiver.
+   */
+  void onTerminateRequest(in nsIPresentationDevice device,
+                          in DOMString presentationId,
+                          in nsIPresentationControlChannel controlChannel,
+                          in boolean aIsFromReceiver);
 };
 
 /*
  * Device provider for any device protocol, can be registered as default
  * providers by adding its contractID to category "presentation-device-provider".
  */
 [scriptable, uuid(3db2578a-0f50-44ad-b01b-28427b71b7bf)]
 interface nsIPresentationDeviceProvider: nsISupports
new file mode 100644
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
@@ -0,0 +1,33 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIPresentationDevice;
+interface nsIPresentationControlChannel;
+
+%{C++
+#define PRESENTATION_TERMINATE_REQUEST_TOPIC "presentation-terminate-request"
+%}
+
+/*
+ * The event of a device requesting for terminating a presentation session. User can
+ * monitor the terminate request on every device by observing "presentation-terminate-request".
+ */
+[scriptable, uuid(3ddbf3a4-53ee-4b70-9bbc-58ac90dce6b5)]
+interface nsIPresentationTerminateRequest: nsISupports
+{
+  // The device which requesting to terminate presentation session.
+  readonly attribute nsIPresentationDevice device;
+
+  // The Id for representing this session.
+  readonly attribute DOMString presentationId;
+
+  // The control channel for this session.
+  // Should only use this channel to complete session termination.
+  readonly attribute nsIPresentationControlChannel controlChannel;
+
+  // True if termination is initiated by receiver.
+  readonly attribute boolean isFromReceiver;
+};
--- a/dom/presentation/ipc/PresentationIPCService.cpp
+++ b/dom/presentation/ipc/PresentationIPCService.cpp
@@ -3,16 +3,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/. */
 
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/PPresentation.h"
 #include "mozilla/ipc/InputStreamUtils.h"
 #include "mozilla/ipc/URIUtils.h"
+#include "nsGlobalWindow.h"
 #include "nsIPresentationListener.h"
 #include "PresentationCallbacks.h"
 #include "PresentationChild.h"
 #include "PresentationContentSessionInfo.h"
 #include "PresentationIPCService.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
@@ -336,16 +337,28 @@ PresentationIPCService::NotifyReceiverRe
   mCallback = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationIPCService::UntrackSessionInfo(const nsAString& aSessionId,
                                            uint8_t aRole)
 {
+  if (nsIPresentationService::ROLE_RECEIVER == aRole) {
+    // Terminate receiver page.
+    uint64_t windowId;
+    if (NS_SUCCEEDED(GetWindowIdBySessionIdInternal(aSessionId, &windowId))) {
+      NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void {
+        if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) {
+          window->Close();
+        }
+      }));
+    }
+  }
+
   // Remove the OOP responding info (if it has never been used).
   RemoveRespondingSessionId(aSessionId);
   if (mSessionInfos.Contains(aSessionId)) {
     mSessionInfos.Remove(aSessionId);
   }
 
   return NS_OK;
 }
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -47,16 +47,17 @@ UNIFIED_SOURCES += [
     'PresentationDeviceManager.cpp',
     'PresentationReceiver.cpp',
     'PresentationRequest.cpp',
     'PresentationService.cpp',
     'PresentationServiceBase.cpp',
     'PresentationSessionInfo.cpp',
     'PresentationSessionRequest.cpp',
     'PresentationTCPSessionTransport.cpp',
+    'PresentationTerminateRequest.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'PresentationDataChannelSessionTransport.js',
     'PresentationDataChannelSessionTransport.manifest',
     'PresentationDeviceInfoManager.js',
     'PresentationDeviceInfoManager.manifest',
 ]
--- a/dom/presentation/provider/ControllerStateMachine.jsm
+++ b/dom/presentation/provider/ControllerStateMachine.jsm
@@ -45,16 +45,22 @@ var handlers = [
     switch (command.type) {
       case CommandType.DISCONNECT:
         stateMachine.state = State.CLOSED;
         stateMachine._notifyDisconnected(command.reason);
         break;
       case CommandType.LAUNCH_ACK:
         stateMachine._notifyLaunch(command.presentationId);
         break;
+      case CommandType.TERMINATE:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
+      case CommandType.TERMINATE_ACK:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
       case CommandType.ANSWER:
       case CommandType.ICE_CANDIDATE:
         stateMachine._notifyChannelDescriptor(command);
         break;
       default:
         debug("unexpected command: " + JSON.stringify(command));
         // ignore unexpected command.
         break;
@@ -82,16 +88,34 @@ ControllerStateMachine.prototype = {
       this._sendCommand({
         type: CommandType.LAUNCH,
         presentationId: presentationId,
         url: url,
       });
     }
   },
 
+  terminate: function _terminate(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE,
+        presentationId: presentationId,
+      });
+    }
+  },
+
+  terminateAck: function _terminateAck(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE_ACK,
+        presentationId: presentationId,
+      });
+    }
+  },
+
   sendOffer: function _sendOffer(offer) {
     if (this.state === State.CONNECTED) {
       this._sendCommand({
         type: CommandType.OFFER,
         offer: offer,
       });
     }
   },
@@ -167,16 +191,20 @@ ControllerStateMachine.prototype = {
   _notifyDisconnected: function _notifyDisconnected(reason) {
     this._channel.notifyDisconnected(reason);
   },
 
   _notifyLaunch: function _notifyLaunch(presentationId) {
     this._channel.notifyLaunch(presentationId);
   },
 
+  _notifyTerminate: function _notifyTerminate(presentationId) {
+    this._channel.notifyTerminate(presentationId);
+  },
+
   _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
     switch (command.type) {
       case CommandType.ANSWER:
         this._channel.notifyAnswer(command.answer);
         break;
       case CommandType.ICE_CANDIDATE:
         this._channel.notifyIceCandidate(command.candidate);
         break;
--- a/dom/presentation/provider/DisplayDeviceProvider.cpp
+++ b/dom/presentation/provider/DisplayDeviceProvider.cpp
@@ -400,16 +400,47 @@ DisplayDeviceProvider::OnSessionRequest(
                                   aControlChannel);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+                                          const nsAString& aPresentationId,
+                                          nsIPresentationControlChannel* aControlChannel,
+                                          bool aIsFromReceiver)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aDeviceInfo);
+  MOZ_ASSERT(aControlChannel);
+
+  nsresult rv;
+
+  nsCOMPtr<nsIPresentationDeviceListener> listener;
+  rv = GetListener(getter_AddRefs(listener));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(!listener);
+
+  rv = listener->OnTerminateRequest(mDevice,
+                                    aPresentationId,
+                                    aControlChannel,
+                                    aIsFromReceiver);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 DisplayDeviceProvider::Observe(nsISupports* aSubject,
                                const char* aTopic,
                                const char16_t* aData)
 {
   if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) {
     nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
--- a/dom/presentation/provider/LegacyPresentationControlService.js
+++ b/dom/presentation/provider/LegacyPresentationControlService.js
@@ -226,16 +226,22 @@ LegacyTCPControlChannel.prototype = {
 
   launch: function(aPresentationId, aUrl) {
     this._presentationId = aPresentationId;
     this._url = aUrl;
 
     this._sendInit();
   },
 
+  terminate: function() {
+    // Legacy protocol doesn't support extra terminate protocol.
+    // Trigger error handling for browser to shutdown all the resource locally.
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
   sendOffer: function(aOffer) {
     let msg = {
       type: "requestSession:Offer",
       presentationId: this._presentationId,
       offer: discriptionAsJson(aOffer),
     };
     this._sendMessage(msg);
   },
--- a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ -912,16 +912,60 @@ MulticastDNSDeviceProvider::OnSessionReq
   if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
     Unused << listener->OnSessionRequest(device, aUrl, aPresentationId,
                                          aControlChannel);
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+                                               const nsAString& aPresentationId,
+                                               nsIPresentationControlChannel* aControlChannel,
+                                               bool aIsFromReceiver)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsAutoCString address;
+  Unused << aDeviceInfo->GetAddress(address);
+
+  LOG_I("OnTerminateRequest: %s", address.get());
+
+  RefPtr<Device> device;
+  uint32_t index;
+  if (FindDeviceByAddress(address, index)) {
+    device = mDevices[index];
+  } else {
+    // Create a one-time device object for non-discoverable controller.
+    // This device will not be in the list of available devices and cannot
+    // be used for requesting session.
+    nsAutoCString id;
+    Unused << aDeviceInfo->GetId(id);
+    uint16_t port;
+    Unused << aDeviceInfo->GetPort(&port);
+
+    device = new Device(id,
+                        /* aName = */ id,
+                        /* aType = */ EmptyCString(),
+                        address,
+                        port,
+                        DeviceState::eActive,
+                        /* aProvider = */ nullptr);
+  }
+
+  nsCOMPtr<nsIPresentationDeviceListener> listener;
+  if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+    Unused << listener->OnTerminateRequest(device, aPresentationId,
+                                           aControlChannel, aIsFromReceiver);
+  }
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 MulticastDNSDeviceProvider::Observe(nsISupports* aSubject,
                                     const char* aTopic,
                                     const char16_t* aData)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
--- a/dom/presentation/provider/PresentationControlService.js
+++ b/dom/presentation/provider/PresentationControlService.js
@@ -161,23 +161,43 @@ PresentationControlService.prototype = {
                                  aDeviceInfo,
                                  "receiver");
   },
 
   // Triggered by TCPControlChannel
   onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
     DEBUG && log("PresentationControlService - onSessionRequest: " +
                  aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+    if (!this.listener) {
+      this.releaseControlChannel(aControlChannel);
+      return;
+    }
+
     this.listener.onSessionRequest(aDeviceInfo,
                                    aUrl,
                                    aPresentationId,
                                    aControlChannel);
     this.releaseControlChannel(aControlChannel);
   },
 
+  onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) {
+    DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
+                 aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+    if (!this.listener) {
+      this.releaseControlChannel(aControlChannel);
+      return;
+    }
+
+    this.listener.onTerminateRequest(aDeviceInfo,
+                                     aPresentationId,
+                                     aControlChannel,
+                                     aIsFromReceiver);
+    this.releaseControlChannel(aControlChannel);
+  },
+
   // nsIServerSocketListener (Triggered by nsIServerSocket.init)
   onSocketAccepted: function(aServerSocket, aClientSocket) {
     DEBUG && log("PresentationControlService - onSocketAccepted: " +
                  aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line
     let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
     this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket));
   },
 
@@ -385,16 +405,26 @@ TCPControlChannel.prototype = {
   sendIceCandidate: function(aCandidate) {
     this._stateMachine.updateIceCandidate(aCandidate);
   },
 
   launch: function(aPresentationId, aUrl) {
     this._stateMachine.launch(aPresentationId, aUrl);
   },
 
+  terminate: function(aPresentationId) {
+    if (!this._terminatingId) {
+      this._terminatingId = aPresentationId;
+      this._stateMachine.terminate(aPresentationId);
+    } else {
+      this._stateMachine.terminateAck(aPresentationId);
+      delete this._terminatingId;
+    }
+  },
+
   // may throw an exception
   _send: function(aMsg) {
     DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); // jshint ignore:line
 
     /**
      * XXX In TCP streaming, it is possible that more than one message in one
      * TCP packet. We use line delimited JSON to identify where one JSON encoded
      * object ends and the next begins. Therefore, we do not allow newline
@@ -645,16 +675,36 @@ TCPControlChannel.prototype = {
         this._presentationService.onSessionRequest(this._deviceInfo,
                                                    url,
                                                    presentationId,
                                                    this);
       break;
     }
   },
 
+  notifyTerminate: function(presentationId) {
+    if (!this._terminatingId) {
+      this._terminatingId = presentationId;
+      this._presentationService.onSessionTerminate(this._deviceInfo,
+                                                  presentationId,
+                                                  this,
+                                                  this._direction === "sender");
+      return;
+    }
+
+    if (this._terminatingId !== presentationId) {
+      // Requested presentation Id doesn't matched with the one in ACK.
+      // Disconnect the control channel with error.
+      DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line
+      this.disconnect(Cr.NS_ERROR_FAILURE);
+    }
+
+    delete this._terminatingId;
+  },
+
   notifyOffer: function(offer) {
     this._onOffer(offer);
   },
 
   notifyAnswer: function(answer) {
     this._onAnswer(answer);
   },
 
--- a/dom/presentation/provider/ReceiverStateMachine.jsm
+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
@@ -53,16 +53,22 @@ var handlers = [
       case CommandType.LAUNCH:
         stateMachine._notifyLaunch(command.presentationId,
                                    command.url);
         stateMachine._sendCommand({
           type: CommandType.LAUNCH_ACK,
           presentationId: command.presentationId
         });
         break;
+      case CommandType.TERMINATE:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
+      case CommandType.TERMINATE_ACK:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
       case CommandType.OFFER:
       case CommandType.ICE_CANDIDATE:
         stateMachine._notifyChannelDescriptor(command);
         break;
       default:
         debug("unexpected command: " + JSON.stringify(command));
         // ignore unexpected command
         break;
@@ -84,16 +90,34 @@ function ReceiverStateMachine(channel) {
 }
 
 ReceiverStateMachine.prototype = {
   launch: function _launch() {
     // presentation session can only be launched by controlling UA.
     debug("receiver shouldn't trigger launch");
   },
 
+  terminate: function _terminate(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE,
+        presentationId: presentationId,
+      });
+    }
+  },
+
+  terminateAck: function _terminateAck(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE_ACK,
+        presentationId: presentationId,
+      });
+    }
+  },
+
   sendOffer: function _sendOffer() {
     // offer can only be sent by controlling UA.
     debug("receiver shouldn't generate offer");
   },
 
   sendAnswer: function _sendAnswer(answer) {
     if (this.state === State.CONNECTED) {
       this._sendCommand({
@@ -166,16 +190,20 @@ ReceiverStateMachine.prototype = {
   _notifyDisconnected: function _notifyDisconnected(reason) {
     this._channel.notifyDisconnected(reason);
   },
 
   _notifyLaunch: function _notifyLaunch(presentationId, url) {
     this._channel.notifyLaunch(presentationId, url);
   },
 
+  _notifyTerminate: function _notifyTerminate(presentationId) {
+    this._channel.notifyTerminate(presentationId);
+  },
+
   _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
     switch (command.type) {
       case CommandType.OFFER:
         this._channel.notifyOffer(command.offer);
         break;
       case CommandType.ICE_CANDIDATE:
         this._channel.notifyIceCandidate(command.candidate);
         break;
--- a/dom/presentation/provider/StateMachineHelper.jsm
+++ b/dom/presentation/provider/StateMachineHelper.jsm
@@ -20,16 +20,18 @@ const State = Object.freeze({
 const CommandType = Object.freeze({
   // control channel life cycle
   CONNECT: "connect", // { deviceId: <string> }
   CONNECT_ACK: "connect-ack", // { presentationId: <string> }
   DISCONNECT: "disconnect", // { reason: <int> }
   // presentation session life cycle
   LAUNCH: "launch", // { presentationId: <string>, url: <string> }
   LAUNCH_ACK: "launch-ack", // { presentationId: <string> }
+  TERMINATE: "terminate", // { presentationId: <string> }
+  TERMINATE_ACK: "terminate-ack", // { presentationId: <string> }
   // session transport establishment
   OFFER: "offer", // { offer: <json> }
   ANSWER: "answer", // { answer: <json> }
   ICE_CANDIDATE: "ice-candidate", // { candidate: <string> }
 });
 
 this.State = State; // jshint ignore:line
 this.CommandType = CommandType; // jshint ignore:line
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -40,17 +40,17 @@ function registerMockedFactory(contractI
 function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
   if (originalFactory) {
     var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
     registrar.unregisterFactory(mockedClassId, mockedFactory);
     registrar.registerFactory(originalClassId, "", contractId, originalFactory);
   }
 }
 
-const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
+var sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
 
 const address = Cc["@mozilla.org/supports-cstring;1"]
                   .createInstance(Ci.nsISupportsCString);
 address.data = "127.0.0.1";
 const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 addresses.appendElement(address, false);
 
 const mockedChannelDescription = {
@@ -137,16 +137,20 @@ const mockedControlChannel = {
         isValid = false;
       }
     } else if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL) {
       isValid = (aSDP.dataChannelSDP == "test-sdp");
     }
     return isValid;
   },
   launch: function(presentationId, url) {
+    sessionId = presentationId;
+  },
+  terminate: function(presentationId) {
+    sendAsyncMessage('sender-terminate', presentationId);
   },
   disconnect: function(reason) {
     sendAsyncMessage('control-channel-closed', reason);
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyDisconnected(reason);
   },
   simulateReceiverReady: function() {
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady();
   },
@@ -387,16 +391,23 @@ addMessageListener('trigger-device-promp
 
 addMessageListener('trigger-incoming-session-request', function(url) {
   var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                       .getService(Ci.nsIPresentationDeviceManager);
   deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
 	       .onSessionRequest(mockedDevice, url, sessionId, mockedControlChannel);
 });
 
+addMessageListener('trigger-incoming-terminate-request', function() {
+  var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
+                      .getService(Ci.nsIPresentationDeviceManager);
+  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
+	       .onTerminateRequest(mockedDevice, sessionId, mockedControlChannel, true);
+});
+
 addMessageListener('trigger-incoming-offer', function() {
   mockedControlChannel.simulateOnOffer();
 });
 
 addMessageListener('trigger-incoming-answer', function() {
   mockedControlChannel.simulateOnAnswer();
 });
 
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -12,17 +12,17 @@ Cu.import('resource://gre/modules/XPCOMU
 const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                       .getService(Ci.nsIUUIDGenerator);
 
 function debug(str) {
   // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n');
 }
 
 const originalFactoryData = [];
-const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
+var sessionId; // Store the uuid generated by PresentationRequest.
 const address = Cc["@mozilla.org/supports-cstring;1"]
                   .createInstance(Ci.nsISupportsCString);
 address.data = "127.0.0.1";
 const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 addresses.appendElement(address, false);
 
 function mockChannelDescription(role) {
   this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]);
@@ -179,24 +179,31 @@ const mockControlChannelOfSender = {
     sendAsyncMessage('offer-sent');
   },
   onAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .onAnswer(answer);
   },
   launch: function(presentationId, url) {
+    sessionId = presentationId;
     sendAsyncMessage('sender-launch', url);
   },
   disconnect: function(reason) {
+    if (!this._listener) {
+      return;
+    }
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyDisconnected(reason);
     mockControlChannelOfReceiver.disconnect();
-  }
+  },
+  terminate: function(presentationId) {
+    sendAsyncMessage('sender-terminate');
+  },
 };
 
 // control channel of receiver
 const mockControlChannelOfReceiver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   set listener(listener) {
     // PresentationPresentingInfo::SetControlChannel
     if (listener) {
@@ -231,20 +238,26 @@ const mockControlChannelOfReceiver = {
   },
   sendAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
         .notifyTransportReady();
     sendAsyncMessage('answer-sent');
   },
   disconnect: function(reason) {
+    if (!this._listener) {
+      return;
+    }
+
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyDisconnected(reason);
     sendAsyncMessage('control-channel-receiver-closed', reason);
+  },
+  terminate: function(presentaionId) {
   }
 };
 
 const mockDevice = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
   id:   'id',
   name: 'name',
   type: 'type',
@@ -362,16 +375,27 @@ function initMockAndListener() {
                           .getService(Ci.nsIPresentationDeviceManager);
     deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                  .onSessionRequest(mockDevice,
                                    url,
                                    sessionId,
                                    mockControlChannelOfReceiver);
   });
 
+  addMessageListener('trigger-on-terminate-request', function() {
+    debug('Got message: trigger-on-terminate-request');
+    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
+                          .getService(Ci.nsIPresentationDeviceManager);
+    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
+                 .onTerminateRequest(mockDevice,
+                                     sessionId,
+                                     mockControlChannelOfReceiver,
+                                     false);
+  });
+
   addMessageListener('trigger-control-channel-open', function(reason) {
     debug('Got message: trigger-control-channel-open');
     mockControlChannelOfSender.notifyConnected();
     mockControlChannelOfReceiver.notifyConnected();
   });
 
   addMessageListener('trigger-on-offer', function() {
     debug('Got message: trigger-on-offer');
--- a/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -106,28 +106,28 @@ function testSendMessage() {
 }
 
 function testConnectionClosed() {
   return new Promise(function(aResolve, aReject) {
     info('Receiver: --- testConnectionClosed ---');
     connection.onclose = function() {
       connection.onclose = null;
       is(connection.state, "closed", "Receiver: Connection should be closed.");
+      command('forward-command', JSON.stringify({ name: 'receiver-closed' }));
       aResolve();
     };
     command('forward-command', JSON.stringify({ name: 'ready-to-close' }));
   });
 }
 
 function runTests() {
   testConnectionAvailable()
   .then(testConnectionReady)
   .then(testIncomingMessage)
   .then(testSendMessage)
-  .then(testConnectionClosed)
-  .then(finish);
+  .then(testConnectionClosed);
 }
 
 runTests();
 
 </script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for B2G PresentationReceiver at receiver side</title>
+  </head>
+  <body>
+    <div id='content'></div>
+<script type='application/javascript;version=1.7'>
+
+'use strict';
+
+function is(a, b, msg) {
+  if (a === b) {
+    alert('OK ' + msg);
+  } else {
+    alert('KO ' + msg + ' | reason: ' + a + ' != ' + b);
+  }
+}
+
+function ok(a, msg) {
+  alert((a ? 'OK ' : 'KO ') + msg);
+}
+
+function info(msg) {
+  alert('INFO ' + msg);
+}
+
+function command(name, data) {
+  alert('COMMAND ' + JSON.stringify({name: name, data: data}));
+}
+
+function finish() {
+  alert('DONE');
+}
+
+var connection;
+
+function testConnectionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionAvailable ---');
+    ok(navigator.presentation, 'Receiver: navigator.presentation should be available.');
+    ok(navigator.presentation.receiver, 'Receiver: navigator.presentation.receiver should be available.');
+
+    navigator.presentation.receiver.connectionList
+    .then((aList) => {
+      is(aList.connections.length, 1, 'Should get one conncetion.');
+      connection = aList.connections[0];
+      ok(connection.id, 'Connection ID should be set: ' + connection.id);
+      is(connection.state, 'connected', 'Connection state at receiver side should be connected.');
+      aResolve();
+    })
+    .catch((aError) => {
+      ok(false, 'Receiver: Error occurred when getting the connection: ' + aError);
+      finish();
+      aReject();
+    });
+  });
+}
+
+function testConnectionReady() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionReady ---');
+    connection.onconnect = function() {
+      connection.onconnect = null;
+      ok(false, 'Should not get |onconnect| event.')
+      aReject();
+    };
+    if (connection.state === 'connected') {
+      connection.onconnect = null;
+      is(connection.state, 'connected', 'Receiver: Connection state should become connected.');
+      aResolve();
+    }
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionTerminate ---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      // Using window.alert at this stage will cause window.close() fail.
+      // Only trigger it if verdict fail.
+      if (connection.state !== 'terminated') {
+        is(connection.state, 'terminated', 'Receiver: Connection should be terminated.');
+      }
+      aResolve();
+     };
+    command('forward-command', JSON.stringify({ name: 'ready-to-terminate' }));
+  });
+}
+
+function runTests() {
+  testConnectionAvailable()
+  .then(testConnectionReady)
+  .then(testConnectionTerminate)
+}
+
+runTests();
+
+</script>
+  </body>
+</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -10,16 +10,18 @@ support-files =
   file_presentation_receiver.html
   file_presentation_receiver_establish_connection_error.html
   file_presentation_receiver_inner_iframe.html
   file_presentation_1ua_wentaway.html
   test_presentation_1ua_connection_wentaway.js
   file_presentation_receiver_auxiliary_navigation.html
   test_presentation_receiver_auxiliary_navigation.js
   file_presentation_sandboxed_presentation.html
+  file_presentation_terminate.html
+  test_presentation_terminate.js
 
 [test_presentation_dc_sender.html]
 [test_presentation_dc_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_dc_receiver_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_1ua_sender_and_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
@@ -41,9 +43,15 @@ skip-if = (e10s || toolkit == 'gonk' || 
 [test_presentation_tcp_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_tcp_receiver_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_receiver_auxiliary_navigation_inproc.html]
 skip-if = (e10s || toolkit == 'gonk')
 [test_presentation_receiver_auxiliary_navigation_oop.html]
 skip-if = (e10s || toolkit == 'gonk')
+[test_presentation_terminate_inproc.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_terminate_oop.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_sender_on_terminate_request.html]
+skip-if = toolkit == 'android'
 [test_presentation_sandboxed_presentation.html]
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -1,11 +1,12 @@
 'use strict';
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 
 function debug(str) {
   // info(str);
 }
 
 var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
 var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_wentaway.html');
 var request;
@@ -139,17 +140,21 @@ function testStartConnection() {
 }
 
 function testConnectionWentaway() {
   return new Promise(function(aResolve, aReject) {
     info('Sender: --- testConnectionWentaway ---');
     connection.onclose = function() {
       connection.onclose = null;
       is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
+      receiverIframe.addEventListener('mozbrowserclose', function closeHandler() {
+        ok(false, 'wentaway should not trigger receiver close');
+        aResolve();
+      });
+      setTimeout(aResolve, 3000);
     };
     gScript.addMessageListener('ready-to-remove-receiverFrame', function onReadyToRemove() {
       gScript.removeMessageListener('ready-to-remove-receiverFrame', onReadyToRemove);
       receiverIframe.src = "http://example.com";
     });
   });
 }
 
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
@@ -65,17 +65,16 @@ function setup() {
       } else if (/^INFO /.exec(message)) {
         info(message.replace(/^INFO /, ""));
       } else if (/^COMMAND /.exec(message)) {
         var command = JSON.parse(message.replace(/^COMMAND /, ""));
         gScript.sendAsyncMessage(command.name, command.data);
       } else if (/^DONE$/.exec(message)) {
         receiverIframe.removeEventListener("mozbrowsershowmodalprompt",
                                             receiverListener);
-        teardown();
       }
     }, false);
 
     var promise = new Promise(function(aResolve, aReject) {
       document.body.appendChild(receiverIframe);
       aResolve(receiverIframe);
     });
 
@@ -173,28 +172,65 @@ function testIncomingMessage() {
       postMessageToIframe('message-from-receiver-received');
       aResolve();
     });
     postMessageToIframe('trigger-message-from-receiver');
   });
 }
 
 function testCloseConnection() {
-  return new Promise(function(aResolve, aReject) {
-    info('Sender: --- testCloseConnection ---');
-    connection.onclose = function() {
-      connection.onclose = null;
-      is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
-    };
-    gScript.addMessageListener('ready-to-close', function onReadyToClose() {
-      gScript.removeMessageListener('ready-to-close', onReadyToClose);
-      connection.close();
+  info('Sender: --- testCloseConnection ---');
+  gScript.addMessageListener('ready-to-close', function onReadyToClose() {
+    gScript.removeMessageListener('ready-to-close', onReadyToClose);
+    connection.close();
+
+    // Test terminate immediate after close.
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      ok(false, "terminate after close should do nothing");
     });
+    connection.terminate();
   });
+
+  return Promise.all([
+    new Promise(function(aResolve, aReject) {
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+        aResolve();
+      };
+    }),
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('receiver-closed', function onReceiverClosed() {
+        gScript.removeMessageListener('receiver-closed', onReceiverClosed);
+        aResolve();
+      });
+    }),
+  ]);
+}
+
+function testTerminateAfterClose() {
+  info('Sender: --- testTerminateAfterClose ---');
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onterminate = function() {
+          connection.onterminate = null;
+          ok(false, 'terminate after close should do nothing');
+          aResolve();
+        };
+        connection.terminate();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'closed', 'Sender: Connection should be closed.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
 }
 
 function teardown() {
   gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
     debug('Got message: teardown-complete');
     gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
     gScript.destroy();
     SimpleTest.finish();
@@ -203,20 +239,23 @@ function teardown() {
   gScript.sendAsyncMessage('teardown');
 }
 
 function runTests() {
   setup().then(testCreateRequest)
          .then(testStartConnection)
          .then(testSendMessage)
          .then(testIncomingMessage)
-         .then(testCloseConnection);
+         .then(testCloseConnection)
+         .then(testTerminateAfterClose)
+         .then(teardown);
 }
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 SpecialPowers.pushPermissions([
   {type: 'presentation-device-manage', allow: false, context: document},
   {type: 'presentation', allow: true, context: document},
   {type: "browser", allow: true, context: document},
 ], () => {
   SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
                                       /* Mocked TCP session transport builder in the test */
                                       ["dom.presentation.session_transport.data_channel.enable", false],
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
@@ -69,17 +69,16 @@ function setup() {
       } else if (/^INFO /.exec(message)) {
         info(message.replace(/^INFO /, ""));
       } else if (/^COMMAND /.exec(message)) {
         var command = JSON.parse(message.replace(/^COMMAND /, ""));
         gScript.sendAsyncMessage(command.name, command.data);
       } else if (/^DONE$/.exec(message)) {
         receiverIframe.removeEventListener("mozbrowsershowmodalprompt",
                                             receiverListener);
-        teardown();
       }
     }, false);
 
     var promise = new Promise(function(aResolve, aReject) {
       document.body.appendChild(receiverIframe);
       aResolve(receiverIframe);
     });
 
@@ -180,28 +179,65 @@ function testIncomingMessage() {
       postMessageToIframe('message-from-receiver-received');
       aResolve();
     });
     postMessageToIframe('trigger-message-from-receiver');
   });
 }
 
 function testCloseConnection() {
-  return new Promise(function(aResolve, aReject) {
-    info('Sender: --- testCloseConnection ---');
-    connection.onclose = function() {
-      connection.onclose = null;
-      is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
-    };
-    gScript.addMessageListener('ready-to-close', function onReadyToClose() {
-      gScript.removeMessageListener('ready-to-close', onReadyToClose);
-      connection.close();
+  info('Sender: --- testCloseConnection ---');
+  gScript.addMessageListener('ready-to-close', function onReadyToClose() {
+    gScript.removeMessageListener('ready-to-close', onReadyToClose);
+    connection.close();
+
+    // Test terminate immediate after close.
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      ok(false, 'terminate after close should do nothing');
     });
+    connection.terminate();
   });
+
+  return Promise.all([
+    new Promise(function(aResolve, aReject) {
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+        aResolve();
+      };
+    }),
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('receiver-closed', function onReceiverClosed() {
+        gScript.removeMessageListener('receiver-closed', onReceiverClosed);
+        aResolve();
+      });
+    }),
+  ]);
+}
+
+function testTerminateAfterClose() {
+  info('Sender: --- testTerminateAfterClose ---');
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onterminate = function() {
+          connection.onterminate = null;
+          ok(false, 'terminate at closed state should do nothing');
+          aResolve();
+        };
+        connection.terminate();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'closed', 'Sender: Connection should be closed.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
 }
 
 function teardown() {
   gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
     gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
     gScript.destroy();
     SimpleTest.finish();
   });
@@ -210,20 +246,23 @@ function teardown() {
 }
 
 function runTests() {
   setup()
   .then(testCreateRequest)
   .then(testStartConnection)
   .then(testSendMessage)
   .then(testIncomingMessage)
-  .then(testCloseConnection);
+  .then(testCloseConnection)
+  .then(testTerminateAfterClose)
+  .then(teardown);
 }
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 SpecialPowers.pushPermissions([
   {type: 'presentation-device-manage', allow: false, context: document},
   {type: 'presentation', allow: true, context: document},
   {type: "browser", allow: true, context: document},
 ], () => {
   SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
                                       /* Mocked TCP session transport builder in the test */
                                       ["dom.presentation.session_transport.data_channel.enable", false],
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test onTerminateRequest at sender side</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276378">Test onTerminateRequest at sender side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var request;
+var connection;
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    request = new PresentationRequest("http://example.com");
+
+    request.getAvailability().then(
+      function(aAvailability) {
+        aAvailability.onchange = function() {
+          aAvailability.onchange = null;
+          ok(aAvailability.value, "Device should be available.");
+          aResolve();
+        }
+      },
+      function(aError) {
+        ok(false, "Error occurred when getting availability: " + aError);
+        teardown();
+        aReject();
+      }
+    );
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+      gScript.removeMessageListener('device-prompt', devicePromptHandler);
+      info("Device prompt is triggered.");
+      gScript.sendAsyncMessage('trigger-device-prompt-select');
+    });
+
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established', controlChannelEstablishedHandler);
+      info("A control channel is established.");
+      gScript.sendAsyncMessage('trigger-control-channel-open');
+    });
+
+    gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler);
+      info("The control channel is opened.");
+    });
+
+    gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler);
+      info("The control channel is closed. " + aReason);
+    });
+
+    gScript.addMessageListener('offer-sent', function offerSentHandler(aIsValid) {
+      gScript.removeMessageListener('offer-sent', offerSentHandler);
+      ok(aIsValid, "A valid offer is sent out.");
+      gScript.sendAsyncMessage('trigger-incoming-transport');
+    });
+
+    gScript.addMessageListener('answer-received', function answerReceivedHandler() {
+      gScript.removeMessageListener('answer-received', answerReceivedHandler);
+      info("An answer is received.");
+    });
+
+    gScript.addMessageListener('data-transport-initialized', function dataTransportInitializedHandler() {
+      gScript.removeMessageListener('data-transport-initialized', dataTransportInitializedHandler);
+      info("Data transport channel is initialized.");
+      gScript.sendAsyncMessage('trigger-incoming-answer');
+    });
+
+    gScript.addMessageListener('data-transport-notification-enabled', function dataTransportNotificationEnabledHandler() {
+      gScript.removeMessageListener('data-transport-notification-enabled', dataTransportNotificationEnabledHandler);
+      info("Data notification is enabled for data transport channel.");
+    });
+
+    var connectionFromEvent;
+    request.onconnectionavailable = function(aEvent) {
+      request.onconnectionavailable = null;
+      connectionFromEvent = aEvent.connection;
+      ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+      if (connection) {
+        is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+      }
+    };
+
+    request.start().then(
+      function(aConnection) {
+        connection = aConnection;
+        ok(connection, "Connection should be available.");
+        ok(connection.id, "Connection ID should be set.");
+        is(connection.state, "connecting", "The initial state should be connecting.");
+
+        if (connectionFromEvent) {
+          is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+        }
+        connection.onconnect = function() {
+          connection.onconnect = null;
+          is(connection.state, "connected", "Connection should be connected.");
+          aResolve();
+        };
+      },
+      function(aError) {
+        ok(false, "Error occurred when establishing a connection: " + aError);
+        teardown();
+        aReject();
+      }
+    );
+  });
+}
+
+function testOnTerminateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler);
+      info("The control channel is opened.");
+    });
+
+    gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler);
+      info("The control channel is closed. " + aReason);
+    });
+
+    gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler);
+      info("The data transport is closed. " + aReason);
+    });
+
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      is(connection.state, "terminated", "Connection should be closed.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-incoming-terminate-request');
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+  testSetup().
+  then(testStartConnection).
+  then(testOnTerminateRequest).
+  then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.presentation.session_transport.data_channel.enable", false]]},
+                            runTests);
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,241 @@
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
+
+function debug(str) {
+  // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate.html');
+var request;
+var connection;
+var receiverIframe;
+
+function setup() {
+  gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+    debug('Got message: device-prompt');
+    gScript.removeMessageListener('device-prompt', devicePromptHandler);
+    gScript.sendAsyncMessage('trigger-device-prompt-select');
+  });
+
+  gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute('remote', oop);
+
+    receiverIframe.setAttribute('src', receiverUrl);
+    receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+      info('Receiver loaded.');
+    });
+
+    // This event is triggered when the iframe calls 'alert'.
+    receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ''));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ''));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ''));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, 'Messaging from iframe complete.');
+        receiverIframe.removeEventListener('mozbrowsershowmodalprompt',
+                                            receiverListener);
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+      aResolve(receiverIframe);
+    });
+
+    var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+                           .getService(SpecialPowers.Ci.nsIObserverService);
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+  });
+
+  gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() {
+    debug('Got message: promise-setup-ready');
+    gScript.removeMessageListener('promise-setup-ready',
+                                  promiseSetupReadyHandler);
+    gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+  });
+
+  gScript.addMessageListener('offer-sent', function offerSentHandler() {
+    debug('Got message: offer-sent');
+    gScript.removeMessageListener('offer-sent', offerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-offer');
+  });
+
+  gScript.addMessageListener('answer-sent', function answerSentHandler() {
+    debug('Got message: answer-sent');
+    gScript.removeMessageListener('answer-sent', answerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-answer');
+  });
+
+  return Promise.resolve();
+}
+
+function testCreateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testCreateRequest ---');
+    request = new PresentationRequest(receiverUrl);
+    request.getAvailability().then((aAvailability) => {
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, 'Sender: Device should be available.');
+        aResolve();
+      }
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when getting availability: ' + aError);
+      teardown();
+      aReject();
+    });
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    request.start().then((aConnection) => {
+      connection = aConnection;
+      ok(connection, 'Sender: Connection should be available.');
+      ok(connection.id, 'Sender: Connection ID should be set.');
+      is(connection.state, 'connecting', 'Sender: The initial state should be connecting.');
+      connection.onconnect = function() {
+        connection.onconnect = null;
+        is(connection.state, 'connected', 'Connection should be connected.');
+        aResolve();
+      };
+
+      info('Sender: test terminate at connecting state');
+      connection.onterminate = function() {
+        connection.onterminate = null;
+        ok(false, 'Should not be able to terminate at connecting state');
+        aReject();
+      }
+      connection.terminate();
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when establishing a connection: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testConnectionTerminate---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+    };
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      gScript.sendAsyncMessage('trigger-control-channel-open');
+    });
+    gScript.addMessageListener('sender-terminate', function senderTerminateHandler() {
+      gScript.removeMessageListener('sender-terminate',
+                                    senderTerminateHandler);
+
+      receiverIframe.addEventListener('mozbrowserclose', function() {
+        ok(true, 'observe receiver page closing');
+        aResolve();
+      });
+
+      gScript.sendAsyncMessage('trigger-on-terminate-request');
+    });
+    gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() {
+      gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate);
+      connection.terminate();
+
+      // test unexpected close right after terminate
+      connection.onclose = function() {
+        ok(false, 'close after terminate should do nothing');
+      };
+      connection.close();
+    });
+  });
+}
+
+function testSendAfterTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    try {
+      connection.send('something');
+      ok(false, 'PresentationConnection.send should be failed');
+    } catch (e) {
+      is(e.name, 'InvalidStateError', 'Must throw InvalidStateError');
+    }
+    aResolve();
+  });
+}
+
+function testCloseAfterTerminate() {
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onclose = function() {
+          connection.onclose = null;
+          ok(false, 'close at terminated state should do nothing');
+          aResolve();
+        };
+        connection.close();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    debug('Got message: teardown-complete');
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().then(testCreateRequest)
+         .then(testStartConnection)
+         .then(testConnectionTerminate)
+         .then(testSendAfterTerminate)
+         .then(testCloseAfterTerminate)
+         .then(teardown);
+}
+
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+  {type: 'browser', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ['dom.presentation.test.enabled', true],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ['dom.ipc.tabs.disabled', false],
+                                      ['dom.presentation.test.stage', 0]]},
+                            runTests);
+});
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+      Test for PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'>
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+      Test for PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'>
+    </script>
+  </body>
+</html>
--- a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -18,16 +18,17 @@ function TestPresentationDevice() {}
 function TestPresentationControlChannel() {}
 
 TestPresentationControlChannel.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   sendOffer: function(offer) {},
   sendAnswer: function(answer) {},
   disconnect: function() {},
   launch: function() {},
+  terminate: function() {},
   set listener(listener) {},
   get listener() {},
 };
 
 var testProvider = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
 
   forceDiscovery: function() {
@@ -134,16 +135,37 @@ function sessionRequest() {
     Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
 
     run_next_test();
   }, 'presentation-session-request', false);
   manager.QueryInterface(Ci.nsIPresentationDeviceListener)
          .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel);
 }
 
+function terminateRequest() {
+  let testUrl = 'http://www.example.org/';
+  let testPresentationId = 'test-presentation-id';
+  let testControlChannel = new TestPresentationControlChannel();
+  let testIsFromReceiver = true;
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest);
+
+    Assert.equal(request.device.id, testDevice.id, 'expected device');
+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+    Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver');
+
+    run_next_test();
+  }, 'presentation-terminate-request', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+         .onTerminateRequest(testDevice, testPresentationId,
+                             testControlChannel, testIsFromReceiver);
+}
+
 function removeDevice() {
   Services.obs.addObserver(function observer(subject, topic, data) {
     Services.obs.removeObserver(observer, topic);
 
     let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
     Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
     Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
     Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
@@ -171,14 +193,15 @@ function removeProvider() {
   manager.removeDeviceProvider(testProvider);
 }
 
 add_test(addProvider);
 add_test(forceDiscovery);
 add_test(addDevice);
 add_test(updateDevice);
 add_test(sessionRequest);
+add_test(terminateRequest);
 add_test(removeDevice);
 add_test(removeProvider);
 
 function run_test() {
   run_next_test();
 }
--- a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -73,16 +73,55 @@ function launch() {
       Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
       Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
 
       run_next_test();
     };
   };
 }
 
+function terminateByController() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  controllerState.terminate(testPresentationId);
+  mockReceiverChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockControllerChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+      run_next_test();
+    };
+
+    receiverState.terminateAck(presentationId);
+  };
+}
+
+function terminateByReceiver() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  receiverState.terminate(testPresentationId);
+  mockControllerChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockReceiverChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+      run_next_test();
+    };
+
+    controllerState.terminateAck(presentationId);
+  };
+}
+
 function exchangeSDP() {
   Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
   Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
 
   const testOffer = 'test-offer';
   const testAnswer = 'test-answer';
   const testIceCandidate = 'test-ice-candidate';
   controllerState.sendOffer(testOffer);
@@ -180,16 +219,18 @@ function abnormalDisconnect() {
       run_next_test();
     };
     controllerState.onChannelClosed(Cr.NS_OK, true);
   };
 }
 
 add_test(connect);
 add_test(launch);
+add_test(terminateByController);
+add_test(terminateByReceiver);
 add_test(exchangeSDP);
 add_test(disconnect);
 add_test(receiverDisconnect);
 add_test(abnormalDisconnect);
 
 function run_test() { // jshint ignore:line
   run_next_test();
 }
--- a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -176,16 +176,185 @@ function testPresentationServer() {
       this.status = 'closed';
       Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify closed');
       yayFuncs.presenterControlChannelClose();
     },
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
   };
 }
 
+function terminateRequest() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected']);
+  let controllerControlChannel;
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiverj) {
+      controllerControlChannel = controlChannel;
+      Assert.equal(deviceInfo.id, pcs.id, 'expected device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      Assert.equal(isFromReceiver, false, 'expected request from controller');
+
+      controllerControlChannel.listener = {
+        notifyConnected: function() {
+          Assert.ok(true, 'control channel notify connected');
+          yayFuncs.controllerControlChannelConnected();
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+          yayFuncs.controllerControlChannelDisconnected();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId', 'http://example.com');
+      presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequest() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected',
+                                   'terminatedByController',
+                                   'terminatedByReceiver']);
+  let controllerControlChannel;
+  let terminatePhase = 'controller';
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      controlChannel.terminate(presentationId); // Reply terminate ack.
+
+      if (terminatePhase === 'controller') {
+        controllerControlChannel = controlChannel;
+        Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+        Assert.equal(isFromReceiver, false, 'expected request from controller');
+        yayFuncs.terminatedByController();
+
+        controllerControlChannel.listener = {
+          notifyConnected: function() {
+            Assert.ok(true, 'control channel notify connected');
+            yayFuncs.controllerControlChannelConnected();
+
+            terminatePhase = 'receiver';
+            controllerControlChannel.terminate('testPresentationId');
+          },
+          notifyDisconnected: function(aReason) {
+            Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+            yayFuncs.controllerControlChannelDisconnected();
+          },
+          QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+        };
+      } else {
+        Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id');
+        Assert.equal(isFromReceiver, true, 'expected request from receiver');
+        yayFuncs.terminatedByReceiver();
+        presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+      }
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequestAbnormal() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected']);
+  let controllerControlChannel;
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      Assert.equal(isFromReceiver, false, 'expected request from controller');
+      controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack.
+
+      controllerControlChannel = controlChannel;
+
+      controllerControlChannel.listener = {
+          notifyConnected: function() {
+          Assert.ok(true, 'control channel notify connected');
+          yayFuncs.controllerControlChannelConnected();
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error');
+          yayFuncs.controllerControlChannelDisconnected();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
 function setOffline() {
   pcs.listener = {
     onPortChange: function(aPort) {
       Assert.notEqual(aPort, 0, 'TCPPresentationServer port changed and the port should be valid');
       pcs.close();
       run_next_test();
     },
   };
@@ -220,16 +389,18 @@ function shutdown()
 
 // Test manually close control channel with NS_ERROR_FAILURE
 function changeCloseReason() {
   CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_ERROR_FAILURE;
   run_next_test();
 }
 
 add_test(loopOfferAnser);
+add_test(terminateRequest);
+add_test(terminateRequestAbnormal);
 add_test(setOffline);
 add_test(changeCloseReason);
 add_test(oneMoreLoop);
 add_test(shutdown);
 
 function run_test() {
   Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
 
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -67,16 +67,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     {name: "NaN", xbl: false},
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -957,18 +958,16 @@ var interfaceNamesInGlobalScope =
     {name: "PresentationSession", disabled: true, permission: ["presentation"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PresentationSessionConnectEvent", disabled: true, permission: ["presentation"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProcessingInstruction",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProgressEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushManager", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscription", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscriptionOptions", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RadioNodeList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/url/URL.cpp
+++ b/dom/url/URL.cpp
@@ -917,75 +917,79 @@ public:
 // This class creates a URL object on the main thread.
 class ConstructorRunnable : public WorkerMainThreadRunnable
 {
 private:
   const nsString mURL;
 
   nsString mBase; // IsVoid() if we have no base URI string.
   RefPtr<URLProxy> mBaseProxy;
-  ErrorResult& mRv;
 
   RefPtr<URLProxy> mRetval;
 
 public:
   ConstructorRunnable(WorkerPrivate* aWorkerPrivate,
-                      const nsAString& aURL, const Optional<nsAString>& aBase,
-                      ErrorResult& aRv)
+                      const nsAString& aURL, const Optional<nsAString>& aBase)
   : WorkerMainThreadRunnable(aWorkerPrivate,
                              NS_LITERAL_CSTRING("URL :: Constructor"))
   , mURL(aURL)
-  , mRv(aRv)
   {
     if (aBase.WasPassed()) {
       mBase = aBase.Value();
     } else {
       mBase.SetIsVoid(true);
     }
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   ConstructorRunnable(WorkerPrivate* aWorkerPrivate,
-                      const nsAString& aURL, URLProxy* aBaseProxy,
-                      ErrorResult& aRv)
+                      const nsAString& aURL, URLProxy* aBaseProxy)
   : WorkerMainThreadRunnable(aWorkerPrivate,
                              NS_LITERAL_CSTRING("URL :: Constructor with BaseURL"))
   , mURL(aURL)
   , mBaseProxy(aBaseProxy)
-  , mRv(aRv)
   {
     mBase.SetIsVoid(true);
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   bool
   MainThreadRun()
   {
     AssertIsOnMainThread();
 
+    ErrorResult rv;
     RefPtr<URLMainThread> url;
     if (mBaseProxy) {
-      url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), rv);
     } else if (!mBase.IsVoid()) {
-      url = URLMainThread::Constructor(nullptr, mURL, mBase, mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, mBase, rv);
     } else {
-      url = URLMainThread::Constructor(nullptr, mURL, nullptr, mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, nullptr, rv);
     }
 
-    if (mRv.Failed()) {
+    if (rv.Failed()) {
+      rv.SuppressException();
       return true;
     }
 
     mRetval = new URLProxy(url.forget());
     return true;
   }
 
   URLProxy*
-  GetURLProxy()
+  GetURLProxy(ErrorResult& aRv) const
   {
+    MOZ_ASSERT(mWorkerPrivate);
+    mWorkerPrivate->AssertIsOnWorkerThread();
+
+    if (!mRetval) {
+      aRv.ThrowTypeError<MSG_INVALID_URL>(mURL);
+    }
+
     return mRetval;
   }
 };
 
 class TeardownURLRunnable : public Runnable
 {
 public:
   explicit TeardownURLRunnable(URLProxy* aURLProxy)
@@ -1206,19 +1210,18 @@ already_AddRefed<URLWorker>
 FinishConstructor(JSContext* aCx, WorkerPrivate* aPrivate,
                   ConstructorRunnable* aRunnable, ErrorResult& aRv)
 {
   aRunnable->Dispatch(aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  RefPtr<URLProxy> proxy = aRunnable->GetURLProxy();
-  if (NS_WARN_IF(!proxy)) {
-    aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+  RefPtr<URLProxy> proxy = aRunnable->GetURLProxy(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   RefPtr<URLWorker> url = new URLWorker(aPrivate, proxy);
   return url.forget();
 }
 
 /* static */ already_AddRefed<URLWorker>
@@ -1227,46 +1230,46 @@ URLWorker::Constructor(const GlobalObjec
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   URLWorker& base = static_cast<URLWorker&>(aBase);
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy(), aRv);
+    new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy());
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ already_AddRefed<URLWorker>
 URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL,
                        const Optional<nsAString>& aBase, ErrorResult& aRv)
 {
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, aBase, aRv);
+    new ConstructorRunnable(workerPrivate, aURL, aBase);
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ already_AddRefed<URLWorker>
 URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL,
                        const nsAString& aBase, ErrorResult& aRv)
 {
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   Optional<nsAString> base;
   base = &aBase;
 
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, base, aRv);
+    new ConstructorRunnable(workerPrivate, aURL, base);
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ void
 URLWorker::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob,
                            const mozilla::dom::objectURLOptions& aOptions,
                            nsAString& aResult, mozilla::ErrorResult& aRv)
--- a/dom/webidl/Geolocation.webidl
+++ b/dom/webidl/Geolocation.webidl
@@ -7,18 +7,18 @@
  * http://www.w3.org/TR/geolocation-API
  *
  * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 dictionary PositionOptions {
   boolean enableHighAccuracy = false;
-  long timeout = 0x7fffffff;
-  long maximumAge = 0;
+  [Clamp] unsigned long timeout = 0x7fffffff;
+  [Clamp] unsigned long maximumAge = 0;
 };
 
 [NoInterfaceObject]
 interface Geolocation {
   [Throws]
   void getCurrentPosition(PositionCallback successCallback,
                           optional PositionErrorCallback? errorCallback = null,
                           optional PositionOptions options);
--- a/dom/workers/ServiceWorkerRegistration.cpp
+++ b/dom/workers/ServiceWorkerRegistration.cpp
@@ -1,22 +1,24 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "ServiceWorkerRegistration.h"
 
+#include "ipc/ErrorIPCUtils.h"
 #include "mozilla/dom/Notification.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
 #include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
+#include "mozilla/unused.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "ServiceWorker.h"
 #include "ServiceWorkerManager.h"
 
 #include "nsIDocument.h"
 #include "nsIServiceWorkerManager.h"
@@ -382,49 +384,51 @@ public:
   {
     mPromise->MaybeReject(aStatus);
   }
 };
 
 class UpdateResultRunnable final : public WorkerRunnable
 {
   RefPtr<PromiseWorkerProxy> mPromiseProxy;
-  ErrorResult mStatus;
+  IPC::Message mSerializedErrorResult;
 
   ~UpdateResultRunnable()
   {}
 
 public:
   UpdateResultRunnable(PromiseWorkerProxy* aPromiseProxy, ErrorResult& aStatus)
     : WorkerRunnable(aPromiseProxy->GetWorkerPrivate())
     , mPromiseProxy(aPromiseProxy)
-    , mStatus(Move(aStatus))
-  { }
+  {
+    // ErrorResult is not thread safe.  Serialize it for transfer across
+    // threads.
+    IPC::WriteParam(&mSerializedErrorResult, aStatus);
+    aStatus.SuppressException();
+  }
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
+    // Deserialize the ErrorResult now that we are back in the worker
+    // thread.
+    ErrorResult status;
+    PickleIterator iter = PickleIterator(mSerializedErrorResult);
+    Unused << IPC::ReadParam(&mSerializedErrorResult, &iter, &status);
+
     Promise* promise = mPromiseProxy->WorkerPromise();
-    if (mStatus.Failed()) {
-      promise->MaybeReject(mStatus);
+    if (status.Failed()) {
+      promise->MaybeReject(status);
     } else {
       promise->MaybeResolve(JS::UndefinedHandleValue);
     }
-    mStatus.SuppressException();
+    status.SuppressException();
     mPromiseProxy->CleanUp();
     return true;
   }
-
-  void
-  PostDispatch(WorkerPrivate* aWorkerPrivate, bool aSuccess) override
-  {
-    if (!aSuccess) {
-      mStatus.SuppressException();
-    }
-  }
 };
 
 class WorkerThreadUpdateCallback final : public ServiceWorkerUpdateFinishCallback
 {
   RefPtr<PromiseWorkerProxy> mPromiseProxy;
 
   ~WorkerThreadUpdateCallback()
   {
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -42,16 +42,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -170,18 +171,16 @@ var interfaceNamesInGlobalScope =
     "PerformanceMark",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserver", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserverEntryList", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushEvent", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushManager", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushMessageData", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscription", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -42,16 +42,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -162,18 +163,16 @@ var interfaceNamesInGlobalScope =
     "PerformanceMark",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserver", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserverEntryList", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushManager", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscription", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscriptionOptions", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Request",