merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 26 Jul 2016 16:57:00 +0200
changeset 348760 67a3a23b722e5386bf621f7fa533197842555a10
parent 348744 81e206ee8ba75828af38658f5485cacdea04a921 (current diff)
parent 348759 ccbf505eff5113af886ca4e3738d476bd0a83ca9 (diff)
child 348800 ceb63dec9267e9bb62f5e5e1f4c9d32d3ac1fbac
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -435,25 +435,32 @@ MenuItem.prototype = {
 
   enabledForContext(contextData) {
     let contexts = getContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
     let docPattern = this.documentUrlMatchPattern;
-    if (docPattern && !docPattern.matches(contextData.pageUrl)) {
+    let pageURI = Services.io.newURI(contextData.pageUrl, null, null);
+    if (docPattern && !docPattern.matches(pageURI)) {
       return false;
     }
 
-    let isMedia = contextData.onImage || contextData.onAudio || contextData.onVideo;
     let targetPattern = this.targetUrlMatchPattern;
-    if (isMedia && targetPattern && !targetPattern.matches(contextData.srcURL)) {
-      // TODO: double check if mediaURL is always set when we need it
-      return false;
+    if (targetPattern) {
+      let isMedia = contextData.onImage || contextData.onAudio || contextData.onVideo;
+      if (!isMedia) {
+        return false;
+      }
+      let srcURI = Services.io.newURI(contextData.srcUrl, null, null);
+      if (!targetPattern.matches(srcURI)) {
+        // TODO: double check if mediaURL is always set when we need it
+        return false;
+      }
     }
 
     return true;
   },
 };
 
 var gExtensionCount = 0;
 /* eslint-disable mozilla/balanced-listeners */
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -27,16 +27,17 @@ support-files =
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_contentscript_connect.js]
 [browser_ext_contextMenus.js]
 [browser_ext_contextMenus_checkboxes.js]
 [browser_ext_contextMenus_icons.js]
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
+[browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_getViews.js]
 [browser_ext_history.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
@@ -0,0 +1,186 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* () {
+  let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser,
+    "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html");
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["contextMenus"],
+    },
+
+    background: function() {
+      // Test menu items using targetUrlPatterns.
+      browser.contextMenus.create({
+        title: "targetUrlPatterns-patternMatches-contextAll",
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "targetUrlPatterns-patternMatches-contextImage",
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["image"],
+      });
+
+      browser.contextMenus.create({
+        title: "targetUrlPatterns-patternDoesNotMatch-contextAll",
+        targetUrlPatterns: ["*://*/does-not-match"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "targetUrlPatterns-patternDoesNotMatch-contextImage",
+        targetUrlPatterns: ["*://*/does-not-match"],
+        contexts: ["image"],
+      });
+
+      // Test menu items using documentUrlPatterns.
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-contextAll",
+        documentUrlPatterns: ["*://*/*context.html"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-contextImage",
+        documentUrlPatterns: ["*://*/*context.html", "http://*/url-that-does-not-match"],
+        contexts: ["image"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-contextAll",
+        documentUrlPatterns: ["*://*/does-not-match"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-contextImage",
+        documentUrlPatterns: ["*://*/does-not-match"],
+        contexts: ["image"],
+      });
+
+      // Test menu items using both targetUrlPatterns and documentUrlPatterns.
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll",
+        documentUrlPatterns: ["*://*/*context.html"],
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll",
+        documentUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll",
+        documentUrlPatterns: ["*://*/*context.html"],
+        targetUrlPatterns: ["*://does-not-match"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll",
+        documentUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://does-not-match"],
+        contexts: ["all"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage",
+        documentUrlPatterns: ["*://*/*context.html"],
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["image"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage",
+        documentUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
+        contexts: ["image"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage",
+        documentUrlPatterns: ["*://*/*context.html"],
+        targetUrlPatterns: ["*://does-not-match"],
+        contexts: ["image"],
+      });
+
+      browser.contextMenus.create({
+        title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage",
+        documentUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://does-not-match"],
+        contexts: ["image"],
+      });
+
+      browser.test.notifyPass("contextmenus-urlPatterns");
+    },
+  });
+
+  function* confirmContextMenuItems(menu, expected) {
+    for (let [label, shouldShow] of expected) {
+      let items = menu.getElementsByAttribute("label", label);
+      if (shouldShow) {
+        is(items.length, 1, `The menu item for label ${label} was correctly shown`);
+      } else {
+        is(items.length, 0, `The menu item for label ${label} was correctly not shown`);
+      }
+    }
+  }
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextmenus-urlPatterns");
+
+  let extensionContextMenu = yield openExtensionContextMenu("#img1");
+  let expected = [
+    ["targetUrlPatterns-patternMatches-contextAll", true],
+    ["targetUrlPatterns-patternMatches-contextImage", true],
+    ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternMatches-contextAll", true],
+    ["documentUrlPatterns-patternMatches-contextImage", true],
+    ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", true],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", true],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+  ];
+  yield confirmContextMenuItems(extensionContextMenu, expected);
+  yield closeContextMenu();
+
+  let contextMenu = yield openContextMenu("body");
+  expected = [
+    ["targetUrlPatterns-patternMatches-contextAll", false],
+    ["targetUrlPatterns-patternMatches-contextImage", false],
+    ["targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternMatches-contextAll", true],
+    ["documentUrlPatterns-patternMatches-contextImage", false],
+    ["documentUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", false],
+    ["documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+    ["documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", false],
+  ];
+  yield confirmContextMenuItems(contextMenu, expected);
+  yield closeContextMenu();
+
+  yield extension.unload();
+  yield BrowserTestUtils.removeTab(tab1);
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -119,33 +119,33 @@ function closeBrowserAction(extension, w
   let group = getBrowserActionWidget(extension);
 
   let node = win.document.getElementById(group.viewId);
   CustomizableUI.hidePanelForNode(node);
 
   return Promise.resolve();
 }
 
-function* openContextMenu(id) {
+function* openContextMenu(selector = "#img1") {
   let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
   let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
-  yield BrowserTestUtils.synthesizeMouseAtCenter(id, {type: "contextmenu", button: 2}, gBrowser.selectedBrowser);
+  yield BrowserTestUtils.synthesizeMouseAtCenter(selector, {type: "contextmenu"}, gBrowser.selectedBrowser);
   yield popupShownPromise;
   return contentAreaContextMenu;
 }
 
 function* closeContextMenu() {
   let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
   let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
   contentAreaContextMenu.hidePopup();
   yield popupHiddenPromise;
 }
 
-function* openExtensionContextMenu() {
-  let contextMenu = yield openContextMenu("#img1");
+function* openExtensionContextMenu(selector = "#img1") {
+  let contextMenu = yield openContextMenu(selector);
   let topLevelMenu = contextMenu.getElementsByAttribute("ext-type", "top-level-menu");
 
   // Return null if the extension only has one item and therefore no extension menu.
   if (topLevelMenu.length == 0) {
     return null;
   }
 
   let extensionMenu = topLevelMenu[0].childNodes[0];
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -96,16 +96,17 @@ support-files =
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
   doc_pretty-print-on-paused.html
   doc_promise-get-allocation-stack.html
   doc_promise-get-fulfillment-stack.html
   doc_promise-get-rejection-stack.html
   doc_promise.html
+  doc_proxy.html
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
   doc_script-eval.html
   doc_script-bookmarklet.html
@@ -513,16 +514,18 @@ skip-if = e10s && debug
 [browser_dbg_variables-view-03.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-04.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-05.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-06.js]
 skip-if = e10s && debug
+[browser_dbg_variables-view-07.js]
+skip-if = e10s && debug
 [browser_dbg_variables-view-accessibility.js]
 subsuite = clipboard
 skip-if = e10s && debug
 [browser_dbg_variables-view-data.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-edit-cancel.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-edit-click.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that proxy objects get their internal state added as pseudo properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_proxy.html";
+
+var test = Task.async(function* () {
+  let options = {
+    source: TAB_URL,
+    line: 1
+  };
+  var dbg = initDebugger(TAB_URL, options);
+  const [tab,, panel] = yield dbg;
+  const debuggerLineNumber = 34;
+  const scopes = waitForCaretAndScopes(panel, debuggerLineNumber);
+  callInTab(tab, "doPause");
+  yield scopes;
+
+  const variables = panel.panelWin.DebuggerView.Variables;
+  ok(variables, "Should get the variables view.");
+
+  const scope = [...variables][0];
+  ok(scope, "Should get the current function's scope.");
+
+  let proxy;
+  [...scope].forEach(function([name, value]) {
+    if(name === "proxy") proxy = value;
+  });
+  ok(proxy, "Should have found the proxy variable");
+
+  info("Expanding variable 'proxy'");
+  let expanded = once(variables, "fetched");
+  proxy.expand();
+  yield expanded;
+
+  let foundTarget = false;
+  let foundHandler = false;
+  for (let [property, data] of proxy) {
+    info("Expanding property '" + property + "'");
+    let expanded = once(variables, "fetched");
+    data.expand();
+    yield expanded;
+    if (property === "<target>") {
+      for(let [subprop, subdata] of data) if(subprop === "name") {
+        is(subdata.value, "target", "The value of '<target>' should be the [[ProxyTarget]]");
+        foundTarget = true;
+      }
+    } else {
+      is(property, "<handler>", "There shouldn't be properties other than <target> and <handler>");
+      for(let [subprop, subdata] of data) if(subprop === "name") {
+        is(subdata.value, "handler", "The value of '<handler>' should be the [[ProxyHandler]]");
+        foundHandler = true;
+      }
+    }
+  }
+  ok(foundTarget, "Should have found the '<target>' property containing the [[ProxyTarget]]");
+  ok(foundHandler, "Should have found the '<handler>' property containing the [[ProxyHandler]]");
+
+  debugger;
+
+  resumeDebuggerThenCloseAndFinish(panel);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_proxy.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger + Proxy test page</title>
+  </head>
+
+  <body>
+    <script>
+    window.target = {name: "target"};
+    window.handler = { /* Debugging a proxy shouldn't run any trap */
+      name: "handler",
+      getPrototypeOf() { throw new Error("proxy getPrototypeOf trap was called"); },
+      setPrototypeOf() { throw new Error("proxy setPrototypeOf trap was called"); },
+      isExtensible() { throw new Error("proxy isExtensible trap was called"); },
+      preventExtensions() { throw new Error("proxy preventExtensions trap was called"); },
+      getOwnPropertyDescriptor() { throw new Error("proxy getOwnPropertyDescriptor trap was called"); },
+      defineProperty() { throw new Error("proxy defineProperty trap was called"); },
+      has() { throw new Error("proxy has trap was called"); },
+      get() { throw new Error("proxy get trap was called"); },
+      set() { throw new Error("proxy set trap was called"); },
+      deleteProperty() { throw new Error("proxy deleteProperty trap was called"); },
+      ownKeys() { throw new Error("proxy ownKeys trap was called"); },
+      apply() { throw new Error("proxy apply trap was called"); },
+      construct() { throw new Error("proxy construct trap was called"); }
+    };
+    window.proxy = new Proxy(target, handler);
+
+    window.doPause = function () {
+      var proxy = window.proxy;
+      debugger;
+    };
+    </script>
+  </body>
+
+</html>
--- a/devtools/client/dom/test/browser.ini
+++ b/devtools/client/dom/test/browser.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
+  page_array.html
   page_basic.html
   !/devtools/client/framework/test/shared-head.js
 
+[browser_dom_array.js]
 [browser_dom_basic.js]
 [browser_dom_refresh.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_array.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_array.html";
+const TEST_ARRAY = [
+  "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+  "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+];
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+  info("Test DOM Panel Array Expansion started");
+
+  let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+  // Expand specified row and wait till children are displayed.
+  yield expandRow(panel, "_a");
+
+  // Verify that children is displayed now.
+  let childRows = getAllRowsForLabel(panel, "_a");
+
+  let item = childRows.pop();
+  is(item.name, "length", "length property is correct");
+  is(item.value, 26, "length property value is 26");
+
+  let i = 0;
+  for (let name in childRows) {
+    let row = childRows[name];
+
+    is(name, i++, `index ${name} is correct and sorted into the correct position`);
+    ok(typeof row.name === "number", "array index is displayed as a number");
+    is(TEST_ARRAY[name], row.value, `value for array[${name}] is ${row.value}`);
+  }
+});
--- a/devtools/client/dom/test/head.js
+++ b/devtools/client/dom/test/head.js
@@ -93,16 +93,84 @@ function synthesizeMouseClickSoon(panel,
 function getRowByLabel(panel, text) {
   let doc = panel.panelWin.document;
   let labels = [...doc.querySelectorAll(".treeLabel")];
   let label = labels.find(node => node.textContent == text);
   return label ? label.closest(".treeRow") : null;
 }
 
 /**
+ * Returns the children (tree row text) of the specified object name as an
+ * array.
+ */
+function getAllRowsForLabel(panel, text) {
+  let rootObjectLevel;
+  let node;
+  let result = [];
+  let doc = panel.panelWin.document;
+  let nodes = [...doc.querySelectorAll(".treeLabel")];
+
+  // Find the label (object name) for which we want the children. We remove
+  // nodes from the start of the array until we reach the property. The children
+  // are then at the start of the array.
+  while (true) {
+    node = nodes.shift();
+
+    if (!node || node.textContent === text) {
+      rootObjectLevel = node.getAttribute("data-level");
+      break;
+    }
+  }
+
+  // Return an empty array if the node is not found.
+  if (!node) {
+    return result;
+  }
+
+  // Now get the children.
+  for (node of nodes) {
+    let level = node.getAttribute("data-level");
+
+    if (level > rootObjectLevel) {
+      result.push({
+        name: normalizeTreeValue(node.textContent),
+        value: normalizeTreeValue(node.parentNode.nextElementSibling.textContent)
+      });
+    } else {
+      break;
+    }
+  }
+
+  return result;
+}
+
+/**
+ * Strings in the tree are in the form ""a"" and numbers in the form "1". We
+ * normalize these values by converting ""a"" to "a" and "1" to 1.
+ *
+ * @param  {String} value
+ *         The value to normalize.
+ * @return {String|Number}
+ *         The normalized value.
+ */
+function normalizeTreeValue(value) {
+  if (value === `""`) {
+    return "";
+  }
+  if (value.startsWith(`"`) && value.endsWith(`"`)) {
+    return value.substr(1, value.length - 2);
+  }
+  if (isFinite(value) && parseInt(value, 10) == value) {
+    return parseInt(value, 10);
+  }
+
+  return value;
+}
+
+/**
  * Expands elements with given label and waits till
  * children are received from the backend.
  */
 function expandRow(panel, labelText) {
   let row = getRowByLabel(panel, labelText);
   return synthesizeMouseClickSoon(panel, row).then(() => {
     // Wait till children (properties) are fetched
     // from the backend.
new file mode 100644
--- /dev/null
+++ b/devtools/client/dom/test/page_array.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>DOM Panel Array Expansion Test Page</title>
+  </head>
+  <body>
+  <h2>DOM Panel Array Expansion Test Page</h2>
+  <script type="text/javascript">
+    "use strict";
+    window._a = [
+      "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+      "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+    ];
+  </script>
+  </body>
+</html>
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -32,16 +32,24 @@ responsive.done=Done
 # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
 # device selector
 responsive.noDeviceSelected=no device selected
 
 # LOCALIZATION NOTE  (responsive.title): the title displayed in the global
 # toolbar
 responsive.title=Responsive Design Mode
 
+# LOCALIZATION NOTE (responsive.enableTouch): tooltip text for the touch
+# simulation button when it's disabled
+responsive.enableTouch=Enable touch simulation
+
+# LOCALIZATION NOTE (responsive.disableTouch): tooltip text for the touch
+# simulation button when it's enabled
+responsive.disableTouch=Disable touch simulation
+
 # LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
 responsive.screenshot=Take a screenshot of the viewport
 
 # LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
 # filename.
 # The first argument (%1$S) is the date string in yyyy-mm-dd format and the
 # second argument (%2$S) is the time string in HH.MM.SS format.
 responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -44,16 +44,18 @@ module.exports = createClass({
       dom.span(
         {
           className: "title",
         },
         getStr("responsive.title")),
       dom.button({
         id: "global-touch-simulation-button",
         className: touchButtonClass,
+        title: (touchSimulation.enabled ?
+          getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
         onClick: () => onUpdateTouchSimulation(!touchSimulation.enabled),
       }),
       dom.button({
         id: "global-screenshot-button",
         className: "toolbar-button devtools-button",
         title: getStr("responsive.screenshot"),
         onClick: onScreenshot,
         disabled: screenshot.isCapturing,
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -148,17 +148,21 @@
 .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.is-active:hover:active a {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
 .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;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/.eslintrc
@@ -0,0 +1,4 @@
+{
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../../../.eslintrc.mochitests"
+}
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -6,16 +6,17 @@ support-files =
 [test_HSplitBox_01.html]
 [test_notification_box_01.html]
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_reps_array.html]
 [test_reps_attribute.html]
 [test_reps_date-time.html]
 [test_reps_document.html]
+[test_reps_event.html]
 [test_reps_function.html]
 [test_reps_grip.html]
 [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]
--- a/devtools/client/shared/components/test/mochitest/head.js
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -1,10 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var { Assert } = require("resource://testing-common/Assert.jsm");
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
--- a/devtools/client/shared/components/test/mochitest/test_reps_array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -9,16 +9,19 @@ Test ArrayRep rep
   <title>Rep test - ArrayRep</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";
+/* import-globals-from head.js */
+
 window.onload = Task.async(function* () {
   let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
   let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
 
   let componentUnderTest = ArrayRep;
   const maxLength = {
     short: 3,
     long: 300
@@ -30,27 +33,31 @@ window.onload = Task.async(function* () 
     // Test property iterator
     yield testMaxProps();
     yield testMoreThanShortMaxProps();
     yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
 
     // Test that properties are rendered as expected by ItemRep
     yield testNested();
-  } catch(e) {
+
+    yield testArray();
+  } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test that correct rep is chosen
     const stub = [];
     const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ArrayRep.rep, `Rep correctly selects ${ArrayRep.rep.displayName}`);
+    is(renderedRep.type, ArrayRep.rep,
+       `Rep correctly selects ${ArrayRep.rep.displayName}`);
+
 
     // Test rendering
     const defaultOutput = `[]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -205,13 +212,46 @@ window.onload = Task.async(function* () 
       {
         mode: "long",
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
   }
+
+  function testArray() {
+    let stub = [
+      "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+      "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+    ];
+
+    const defaultOutput = `["a", "b", "c", "d", "e", "f", "g", "h", "i", "j",` +
+                          ` "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",` +
+                          ` "u", "v", "w", "x", "y", "z"]`;
+    const shortOutput = `["a", "b", "c", moreā€¦]`;
+
+    const modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: shortOutput,
+      },
+      {
+        mode: "tiny",
+        expectedOutput: `[26]`,
+      },
+      {
+        mode: "short",
+        expectedOutput: shortOutput,
+      },
+      {
+        mode: "long",
+        expectedOutput: defaultOutput,
+      }
+    ];
+
+    testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+  }
 });
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_event.html
@@ -0,0 +1,290 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Event rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - Event</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">
+window.onload = Task.async(function* () {
+  let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+  let { Event } = browserRequire("devtools/client/shared/components/reps/event");
+
+  try {
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testEvent") });
+    is(renderedRep.type, Event.rep, `Rep correctly selects ${Event.rep.displayName}`);
+
+    yield testEvent();
+    yield testMouseEvent();
+    yield testKeyboardEvent();
+    yield testMessageEvent();
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+
+  function testEvent() {
+    const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testEvent") });
+    is(renderedComponent.textContent, "beforeprint", "Event rep has expected text content for an event");
+  }
+
+  function testMouseEvent() {
+    const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMouseEvent") });
+    is(renderedComponent.textContent, "clickclientX=62, clientY=18", "Event rep has expected text content for a mouse event");
+  }
+
+  function testKeyboardEvent() {
+    const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testKeyboardEvent") });
+    is(renderedComponent.textContent, "keyupcharCode=0, keyCode=17", "Event rep has expected text content for a keyboard event");
+  }
+
+  function testMessageEvent() {
+    const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMessageEvent") });
+    is(renderedComponent.textContent, "messageorigin=null, data=test data", "Event rep has expected text content for a message event");
+  }
+
+  function getGripStub(name) {
+    switch (name) {
+      case "testEvent":
+        return {
+          "type": "object",
+          "class": "Event",
+          "actor": "server1.conn23.obj35",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 1,
+          "preview": {
+            "kind": "DOMEvent",
+            "type": "beforeprint",
+            "properties": {
+              "isTrusted": true,
+              "currentTarget": {
+                "type": "object",
+                "class": "Window",
+                "actor": "server1.conn23.obj37",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 760,
+                "preview": {
+                  "kind": "ObjectWithURL",
+                  "url": "http://example.com"
+                }
+              },
+              "eventPhase": 2,
+              "bubbles": false,
+              "cancelable": false,
+              "defaultPrevented": false,
+              "timeStamp": 1466780008434005,
+              "originalTarget": {
+                "type": "object",
+                "class": "Window",
+                "actor": "server1.conn23.obj38",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 760,
+                "preview": {
+                  "kind": "ObjectWithURL",
+                  "url": "http://example.com"
+                }
+              },
+              "explicitOriginalTarget": {
+                "type": "object",
+                "class": "Window",
+                "actor": "server1.conn23.obj39",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 760,
+                "preview": {
+                  "kind": "ObjectWithURL",
+                  "url": "http://example.com"
+                }
+              },
+              "NONE": 0
+            },
+            "target": {
+              "type": "object",
+              "class": "Window",
+              "actor": "server1.conn23.obj36",
+              "extensible": true,
+              "frozen": false,
+              "sealed": false,
+              "ownPropertyLength": 760,
+              "preview": {
+                "kind": "ObjectWithURL",
+                "url": "http://example.com"
+              }
+            }
+          }
+        };
+
+      case "testMouseEvent":
+        return {
+          "type": "object",
+          "class": "MouseEvent",
+          "actor": "server1.conn20.obj39",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 1,
+          "preview": {
+            "kind": "DOMEvent",
+            "type": "click",
+            "properties": {
+              "buttons": 0,
+              "clientX": 62,
+              "clientY": 18,
+              "layerX": 0,
+              "layerY": 0
+            },
+            "target": {
+              "type": "object",
+              "class": "HTMLDivElement",
+              "actor": "server1.conn20.obj40",
+              "extensible": true,
+              "frozen": false,
+              "sealed": false,
+              "ownPropertyLength": 0,
+              "preview": {
+                "kind": "DOMNode",
+                "nodeType": 1,
+                "nodeName": "div",
+                "attributes": {
+                  "id": "test"
+                },
+                "attributesLength": 1
+              }
+            }
+          }
+        };
+
+      case "testKeyboardEvent":
+        return {
+          "type": "object",
+          "class": "KeyboardEvent",
+          "actor": "server1.conn21.obj49",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 1,
+          "preview": {
+            "kind": "DOMEvent",
+            "type": "keyup",
+            "properties": {
+              "key": "Control",
+              "charCode": 0,
+              "keyCode": 17
+            },
+            "target": {
+              "type": "object",
+              "class": "HTMLBodyElement",
+              "actor": "server1.conn21.obj50",
+              "extensible": true,
+              "frozen": false,
+              "sealed": false,
+              "ownPropertyLength": 0,
+              "preview": {
+                "kind": "DOMNode",
+                "nodeType": 1,
+                "nodeName": "body",
+                "attributes": {},
+                "attributesLength": 0
+              }
+            },
+            "eventKind": "key",
+            "modifiers": []
+          }
+        };
+
+      case "testMessageEvent":
+        return {
+          "type": "object",
+          "class": "MessageEvent",
+          "actor": "server1.conn3.obj34",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 1,
+          "preview": {
+            "kind": "DOMEvent",
+            "type": "message",
+            "properties": {
+              "isTrusted": false,
+              "data": "test data",
+              "origin": "null",
+              "lastEventId": "",
+              "source": {
+                "type": "object",
+                "class": "Window",
+                "actor": "server1.conn3.obj36",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 760,
+                "preview": {
+                  "kind": "ObjectWithURL",
+                  "url": ""
+                }
+              },
+              "ports": {
+                "type": "object",
+                "class": "MessagePortList",
+                "actor": "server1.conn3.obj37",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0
+              },
+              "currentTarget": {
+                "type": "object",
+                "class": "Window",
+                "actor": "server1.conn3.obj38",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 760,
+                "preview": {
+                  "kind": "ObjectWithURL",
+                  "url": ""
+                }
+              },
+              "eventPhase": 2,
+              "bubbles": false,
+              "cancelable": false
+            },
+            "target": {
+              "type": "object",
+              "class": "Window",
+              "actor": "server1.conn3.obj35",
+              "extensible": true,
+              "frozen": false,
+              "sealed": false,
+              "ownPropertyLength": 760,
+              "preview": {
+                "kind": "ObjectWithURL",
+                "url": ""
+              }
+            }
+          }
+        };
+
+    }
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/shared/components/tree/label-cell.js
+++ b/devtools/client/shared/components/tree/label-cell.js
@@ -37,19 +37,20 @@ define(function (require, exports, modul
       };
 
       return (
         td({
           className: "treeLabelCell",
           key: "default",
           style: rowStyle},
           span({ className: "treeIcon" }),
-          span({ className: "treeLabel " + member.type + "Label" },
-            member.name
-          )
+          span({
+            className: "treeLabel " + member.type + "Label",
+            "data-level": level
+          }, member.name)
         )
       );
     }
   });
 
   // Exports from this module
   module.exports = LabelCell;
 });
new file mode 100755
--- /dev/null
+++ b/devtools/client/shared/vendor/jsol.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+(function () {
+  /**
+   JSOL stands for JavaScript Object Literal which is a string representing
+   an object in JavaScript syntax.
+
+   For example:
+
+   {foo:"bar"} is equivalent to {"foo":"bar"} in JavaScript. Both are valid JSOL.
+
+   Note that {"foo":"bar"} is proper JSON[1] therefore you can use one of the many
+   JSON parsers out there like json2.js[2] or even the native browser's JSON parser,
+   if available.
+
+   However, {foo:"bar"} is NOT proper JSON but valid Javascript syntax for
+   representing an object with one key, "foo" and its value, "bar".
+   Using a JSON parser is not an option since this is NOT proper JSON.
+
+   You can use JSOL.parse to safely parse any string that reprsents a JavaScript Object Literal.
+   JSOL.parse will throw an Invalid JSOL exception on function calls, function declarations and variable references.
+
+   Examples:
+
+   JSOL.parse('{foo:"bar"}');  // valid
+
+   JSOL.parse('{evil:(function(){alert("I\'m evil");})()}');  // invalid function calls
+
+   JSOL.parse('{fn:function() { }}'); // invalid function declarations
+
+   var bar = "bar";
+   JSOL.parse('{foo:bar}');  // invalid variable references
+
+   [1] http://www.json.org
+   [2] http://www.json.org/json2.js
+   */
+  var trim =  /^(\s|\u00A0)+|(\s|\u00A0)+$/g; // Used for trimming whitespace
+  var JSOL = {
+    parse: function(text) {
+      // make sure text is a "string"
+      if (typeof text !== "string" || !text) {
+        return null;
+      }
+      // Make sure leading/trailing whitespace is removed
+      text = text.replace(trim, "");
+      // Make sure the incoming text is actual JSOL (or Javascript Object Literal)
+      // Logic borrowed from http://json.org/json2.js
+      if ( /^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
+           .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
+           .replace(/(?:^|:|,)(?:\s*\[)+/g, ":")
+           /** everything up to this point is json2.js **/
+           /** this is the 5th stage where it accepts unquoted keys **/
+           .replace(/\w*\s*\:/g, ":")) ) {
+        return (new Function("return " + text))();
+      }
+      else {
+        throw("Invalid JSOL: " + text);
+      }
+    }
+  };
+
+  if (typeof define === "function" && define.amd) {
+    define(JSOL);
+  } else if (typeof module === "object" && module.exports) {
+    module.exports = JSOL;
+  } else {
+    this.JSOL = JSOL;
+  }
+})();
--- a/devtools/client/shared/vendor/moz.build
+++ b/devtools/client/shared/vendor/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; 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/.
 modules = []
 modules += [
-    'immutable.js'
+    'immutable.js',
+    'jsol.js'
 ]
 
 # react-dev is used if either debug mode is enabled,
 # so include it for both
 if CONFIG['DEBUG_JS_MODULES'] or CONFIG['MOZ_DEBUG']:
     modules += ['react-dev.js']
 modules += [
     'react-dom.js',
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -325,16 +325,30 @@ VariablesViewController.prototype = {
    * when a scope is expanded or certain variables are hovered.
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function (aTarget, aGrip) {
+    if (aGrip.class === "Proxy") {
+      this.addExpander(
+        aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
+        aGrip.proxyTarget);
+      this.addExpander(
+        aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
+        aGrip.proxyHandler);
+
+      // Refuse to play the proxy's stupid game and return immediately
+      let deferred = defer();
+      deferred.resolve();
+      return deferred.promise;
+    }
+    
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state }, { internalItem: true });
       if (state === "fulfilled") {
         this.addExpander(
           aTarget.addItem("<value>", { value }, { internalItem: true }),
           value);
       } else if (state === "rejected") {
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Task} = require("devtools/shared/task");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {LocalizationHelper} = require("devtools/client/shared/l10n");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const JSOL = require("devtools/client/shared/vendor/jsol");
 
 loader.lazyRequireGetter(this, "TreeWidget",
                          "devtools/client/shared/widgets/TreeWidget", true);
 loader.lazyRequireGetter(this, "TableWidget",
                          "devtools/client/shared/widgets/TableWidget", true);
 loader.lazyRequireGetter(this, "ViewHelpers",
                          "devtools/client/shared/widgets/view-helpers");
 loader.lazyImporter(this, "VariablesView",
@@ -219,22 +220,25 @@ StorageUI.prototype = {
   },
 
   getCurrentActor: function () {
     let type = this.table.datatype;
 
     return this.storageTypes[type];
   },
 
-  makeFieldsEditable: function* () {
-    let actor = this.getCurrentActor();
-
-    if (typeof actor.getEditableFields !== "undefined") {
-      let fields = yield actor.getEditableFields();
-      this.table.makeFieldsEditable(fields);
+  /**
+   *  Make column fields editable
+   *
+   *  @param {Array} editableFields
+   *         An array of keys of columns to be made editable
+   */
+  makeFieldsEditable: function* (editableFields) {
+    if (editableFields && editableFields.length > 0) {
+      this.table.makeFieldsEditable(editableFields);
     } else if (this.table._editableFieldsEngine) {
       this.table._editableFieldsEngine.destroy();
     }
   },
 
   editItem: function (eventType, data) {
     let actor = this.getCurrentActor();
 
@@ -480,21 +484,35 @@ StorageUI.prototype = {
     if (reason !== REASON.NEXT_50_ITEMS &&
         reason !== REASON.UPDATE &&
         reason !== REASON.NEW_ROW &&
         reason !== REASON.POPULATE) {
       throw new Error("Invalid reason specified");
     }
 
     try {
+      if (reason === REASON.POPULATE) {
+        let subType = null;
+        // The indexedDB type could have sub-type data to fetch.
+        // If having names specified, then it means
+        // we are fetching details of specific database or of object store.
+        if (type == "indexedDB" && names) {
+          let [ dbName, objectStoreName ] = JSON.parse(names[0]);
+          if (dbName) {
+            subType = "database";
+          }
+          if (objectStoreName) {
+            subType = "object store";
+          }
+        }
+        yield this.resetColumns(type, host, subType);
+      }
+
       let {data} = yield storageType.getStoreObjects(host, names, fetchOpts);
       if (data.length) {
-        if (reason === REASON.POPULATE) {
-          yield this.resetColumns(data[0], type, host);
-        }
         this.populateTable(data, reason);
       }
       this.emit("store-objects-updated");
     } catch (ex) {
       console.error(ex);
     }
   }),
 
@@ -613,17 +631,17 @@ StorageUI.prototype = {
    * @param {string} name
    *        The key corresponding to the `value` string in the object
    * @param {string} value
    *        The string to be parsed into an object
    */
   parseItemValue: function (name, value) {
     let json = null;
     try {
-      json = JSON.parse(value);
+      json = JSOL.parse(value);
     } catch (ex) {
       json = null;
     }
 
     if (!json && value) {
       json = this._extractKeyValPairs(value);
     }
 
@@ -725,46 +743,56 @@ StorageUI.prototype = {
     }
     this.fetchStorageObjects(type, host, names, REASON.POPULATE);
     this.itemOffset = 0;
   },
 
   /**
    * Resets the column headers in the storage table with the pased object `data`
    *
-   * @param {object} data
-   *        The object from which key and values will be used for naming the
-   *        headers of the columns
    * @param {string} type
    *        The type of storage corresponding to the after-reset columns in the
    *        table.
    * @param {string} host
    *        The host name corresponding to the table after reset.
+   *
+   * @param {string} [subType]
+   *        The sub type under the given type.
    */
-  resetColumns: function* (data, type, host) {
+  resetColumns: function* (type, host, subtype) {
+    this.table.host = host;
+    this.table.datatype = type;
+
+    let uniqueKey = null;
     let columns = {};
-    let uniqueKey = null;
-    for (let key in data) {
+    let editableFields = [];
+    let fields = yield this.getCurrentActor().getFields(subtype);
+
+    fields.forEach(f => {
       if (!uniqueKey) {
-        this.table.uniqueId = uniqueKey = key;
+        this.table.uniqueId = uniqueKey = f.name;
       }
-      columns[key] = key;
+
+      if (f.editable) {
+        editableFields.push(f.name);
+      }
+
+      columns[f.name] = f.name;
       try {
-        columns[key] = L10N.getStr("table.headers." + type + "." + key);
+        columns[f.name] = L10N.getStr("table.headers." + type + "." + f.name);
       } catch (e) {
         console.error("Unable to localize table header type:" + type +
-                      " key:" + key);
+                      " key:" + f.name);
       }
-    }
+    });
+
     this.table.setColumns(columns, null, HIDDEN_COLUMNS);
-    this.table.datatype = type;
-    this.table.host = host;
     this.hideSidebar();
 
-    yield this.makeFieldsEditable();
+    yield this.makeFieldsEditable(editableFields);
   },
 
   /**
    * Populates or updates the rows in the storage table.
    *
    * @param {array[object]} data
    *        Array of objects to be populated in the storage table
    * @param {Constant} reason
--- a/devtools/client/webconsole/test/browser_webconsole_output_05.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_05.js
@@ -145,16 +145,25 @@ var inputTests = [
   {
     input: "new Object({1: 'this\\nis\\nsupposed\\nto\\nbe\\na\\nvery" +
            "\\nlong\\nstring\\n,shown\\non\\na\\nsingle\\nline', " +
            "2: 'a shorter string', 3: 100})",
     output: 'Object { 1: "this is supposed to be a very long ' + ELLIPSIS +
             '", 2: "a shorter string", 3: 100 }',
     printOutput: "[object Object]",
     inspectable: false,
+  },
+
+  // 15
+  {
+    input: "new Proxy({a:1},[1,2,3])",
+    output: 'Proxy { <target>: Object, <handler>: Array[3] }',
+    printOutput: "[object Object]",
+    inspectable: true,
+    variablesViewLabel: "Proxy"
   }
 ];
 
 function test() {
   requestLongerTimeout(2);
   Task.spawn(function* () {
     let {tab} = yield loadTab(TEST_URI);
     let hud = yield openConsole(tab);
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -76,51 +76,60 @@ ObjectActor.prototype = {
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
   grip: function () {
     this.hooks.incrementGripDepth();
 
     let g = {
       "type": "object",
-      "class": this.obj.class,
-      "actor": this.actorID,
-      "extensible": this.obj.isExtensible(),
-      "frozen": this.obj.isFrozen(),
-      "sealed": this.obj.isSealed()
+      "actor": this.actorID
     };
 
-    if (this.obj.class != "DeadObject") {
-      if (this.obj.class == "Promise") {
+    // If it's a proxy, lie and tell that it belongs to an invented
+    // "Proxy" class, and avoid calling the [[IsExtensible]] trap
+    if(this.obj.isProxy) {
+      g.class = "Proxy";
+      g.proxyTarget = this.hooks.createValueGrip(this.obj.proxyTarget);
+      g.proxyHandler = this.hooks.createValueGrip(this.obj.proxyHandler);
+    } else {
+      g.class = this.obj.class;
+      g.extensible = this.obj.isExtensible();
+      g.frozen = this.obj.isFrozen();
+      g.sealed = this.obj.isSealed();
+    }
+
+    if (g.class != "DeadObject") {
+      if (g.class == "Promise") {
         g.promiseState = this._createPromiseState();
       }
 
       // FF40+: Allow to know how many properties an object has
       // to lazily display them when there is a bunch.
       // Throws on some MouseEvent object in tests.
       try {
         // Bug 1163520: Assert on internal functions
-        if (this.obj.class != "Function") {
+        if (!["Function", "Proxy"].includes(g.class)) {
           g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
         }
       } catch (e) {}
 
       let raw = this.obj.unsafeDereference();
 
       // If Cu is not defined, we are running on a worker thread, where xrays
       // don't exist.
       if (Cu) {
         raw = Cu.unwaiveXrays(raw);
       }
 
       if (!DevToolsUtils.isSafeJSObject(raw)) {
         raw = null;
       }
 
-      let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] ||
+      let previewers = DebuggerServer.ObjectActorPreviewers[g.class] ||
                        DebuggerServer.ObjectActorPreviewers.Object;
       for (let fn of previewers) {
         try {
           if (fn(this, g, raw)) {
             break;
           }
         } catch (e) {
           let msg = "ObjectActor.prototype.grip previewer function";
@@ -1331,16 +1340,33 @@ DebuggerServer.ObjectActorPreviewers = {
       entries.push([key, hooks.createValueGrip(value)]);
       if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
+
+  Proxy: [function ({obj, hooks}, grip, rawObj) {
+    grip.preview = {
+      kind: "Object",
+      ownProperties: Object.create(null),
+      ownPropertiesLength: 2
+    };
+
+    if (hooks.getGripDepth() > 1) {
+      return true;
+    }
+
+    grip.preview.ownProperties['<target>'] = {value: grip.proxyTarget};
+    grip.preview.ownProperties['<handler>'] = {value: grip.proxyHandler};
+
+    return true;
+  }],
 };
 
 /**
  * Generic previewer for classes wrapping primitives, like String,
  * Number and Boolean.
  *
  * @param string className
  *        Class name to expect.
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -76,16 +76,20 @@ var StorageActors = {};
  *               topic.
  *   - getNamesForHost : Given a host, get list of all known store names.
  *   - getValuesForHost : Given a host (and optianally a name) get all known
  *                        store objects.
  *   - toStoreObject : Given a store object, convert it to the required format
  *                     so that it can be transferred over wire.
  *   - populateStoresForHost : Given a host, populate the map of all store
  *                             objects for it
+ *   - getFields: Given a subType(optional), get an array of objects containing
+ *                column field info. The info includes,
+ *                "name" is name of colume key.
+ *                "editable" is 1 means editable field; 0 means uneditable.
  *
  * @param {string} typeName
  *        The typeName of the actor.
  * @param {string} observationTopic
  *        The topic which this actor listens to via Notification Observers.
  */
 StorageActors.defaults = function (typeName, observationTopic) {
   return {
@@ -543,31 +547,27 @@ StorageActors.createActor({
           }
           this.storageActor.update("cleared", "cookies", data);
         }
         break;
     }
     return null;
   },
 
-  /**
-   * This method marks the table as editable.
-   *
-   * @return {Array}
-   *         An array of column header ids.
-   */
-  getEditableFields: Task.async(function* () {
+  getFields: Task.async(function* () {
     return [
-      "name",
-      "path",
-      "host",
-      "expires",
-      "value",
-      "isSecure",
-      "isHttpOnly"
+      { name: "name", editable: 1},
+      { name: "path", editable: 1},
+      { name: "host", editable: 1},
+      { name: "expires", editable: 1},
+      { name: "lastAccessed", editable: 0},
+      { name: "value", editable: 1},
+      { name: "isDomain", editable: 0},
+      { name: "isSecure", editable: 1},
+      { name: "isHttpOnly", editable: 1}
     ];
   }),
 
   /**
    * Pass the editItem command from the content to the chrome process.
    *
    * @param {Object} data
    *        See editCookie() for format details.
@@ -1007,26 +1007,20 @@ function getObjectForLocalOrSessionStora
 
     populateStoresForHosts() {
       this.hostVsStores = new Map();
       for (let window of this.windows) {
         this.populateStoresForHost(this.getHostName(window.location), window);
       }
     },
 
-    /**
-     * This method marks the fields as editable.
-     *
-     * @return {Array}
-     *         An array of field ids.
-     */
-    getEditableFields: Task.async(function* () {
+    getFields: Task.async(function* () {
       return [
-        "name",
-        "value"
+        { name: "name", editable: 1},
+        { name: "value", editable: 1}
       ];
     }),
 
     /**
      * Edit localStorage or sessionStorage fields.
      *
      * @param {Object} data
      *        See editCookie() for format details.
@@ -1194,16 +1188,23 @@ StorageActors.createActor({
 
   processEntry: Task.async(function* (request, response) {
     return {
       url: String(request.url),
       status: String(response.statusText),
     };
   }),
 
+  getFields: Task.async(function* () {
+    return [
+      { name: "url", editable: 0 },
+      { name: "status", editable: 0 }
+    ];
+  }),
+
   getHostName(location) {
     if (!location.host) {
       return location.href;
     }
     return location.protocol + "//" + location.host;
   },
 
   populateStoresForHost: Task.async(function* (host) {
@@ -1648,16 +1649,45 @@ StorageActors.createActor({
       sendAsyncMessage("storage:storage-indexedDB-request-parent", {
         method: methodName,
         args: args
       });
 
       return deferred.promise;
     }
   },
+
+  getFields: Task.async(function* (subType) {
+    switch (subType) {
+      // Detail of database
+      case "database":
+        return [
+          { name: "objectStore", editable: 0 },
+          { name: "keyPath", editable: 0 },
+          { name: "autoIncrement", editable: 0 },
+          { name: "indexes", editable: 0 },
+        ];
+
+      // Detail of object store
+      case "object store":
+        return [
+          { name: "name", editable: 0 },
+          { name: "value", editable: 0 }
+        ];
+
+      // Detail of indexedDB for one origin
+      default:
+        return [
+          { name: "db", editable: 0 },
+          { name: "origin", editable: 0 },
+          { name: "version", editable: 0 },
+          { name: "objectStores", editable: 0 },
+        ];
+    }
+  })
 });
 
 var indexedDBHelpers = {
   backToChild(...args) {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
 
     mm.broadcastAsyncMessage("storage:storage-indexedDB-request-child", {
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -13,16 +13,24 @@ function createStorageSpec(options) {
   let methods = {
     getStoreObjects: {
       request: {
         host: Arg(0),
         names: Arg(1, "nullable:array:string"),
         options: Arg(2, "nullable:json")
       },
       response: RetVal(options.storeObjectType)
+    },
+    getFields: {
+      request: {
+        subType: Arg(0, "nullable:string")
+      },
+      response: {
+        value: RetVal("json")
+      }
     }
   };
 
   // extra methods specific for storage type
   Object.assign(methods, options.methods);
 
   childSpecs[options.typeName] = protocol.generateActorSpec({
     typeName: options.typeName,
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -640,16 +640,17 @@ nsContentSecurityManager::IsOriginPotent
   // trust to other, vendor-specific URL schemes. We use this for "resource:",
   // which is technically a substituting protocol handler that is not limited to
   // local resource mapping, but in practice is never mapped remotely as this
   // would violate assumptions a lot of code makes.
   if (scheme.EqualsLiteral("https") ||
       scheme.EqualsLiteral("file") ||
       scheme.EqualsLiteral("resource") ||
       scheme.EqualsLiteral("app") ||
+      scheme.EqualsLiteral("moz-extension") ||
       scheme.EqualsLiteral("wss")) {
     *aIsTrustWorthy = true;
     return NS_OK;
   }
 
   nsAutoCString host;
   rv = uri->GetHost(host);
   if (NS_FAILED(rv)) {
--- a/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js
+++ b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js
@@ -22,16 +22,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 add_task(function* test_isOriginPotentiallyTrustworthy() {
   for (let [uriSpec, expectedResult] of [
     ["http://example.com/", false],
     ["https://example.com/", true],
     ["http://localhost/", true],
     ["http://127.0.0.1/", true],
     ["file:///", true],
     ["resource:///", true],
+    ["moz-extension://", true],
     ["about:config", false],
     ["urn:generic", false],
   ]) {
     let uri = NetUtil.newURI(uriSpec);
     let principal = gScriptSecurityManager.getCodebasePrincipal(uri);
     Assert.equal(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal),
                  expectedResult);
   }
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
@@ -118,22 +118,24 @@ public class RecentTabsAdapter extends R
 
                 // Update the "Recently closed" part of the tab list.
                 updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
             }
         });
     }
 
     private void readPreviousSessionData() {
-        // Make sure that the start up code has had a chance to update sessionstore.bak as necessary.
-        GeckoProfile.get(context).waitForOldSessionDataProcessing();
-
-        ThreadUtils.postToBackgroundThread(new Runnable() {
+        // If we happen to initialise before GeckoApp, waiting on either the main or the background
+        // thread can lead to a deadlock, so we have to run on a separate thread instead.
+        final Thread parseThread = new Thread(new Runnable() {
             @Override
             public void run() {
+                // Make sure that the start up code has had a chance to update sessionstore.bak as necessary.
+                GeckoProfile.get(context).waitForOldSessionDataProcessing();
+
                 final String jsonString = GeckoProfile.get(context).readSessionFile(true);
                 if (jsonString == null) {
                     // No previous session data.
                     return;
                 }
 
                 final List<ClosedTab> parsedTabs = new ArrayList<>();
 
@@ -170,17 +172,19 @@ public class RecentTabsAdapter extends R
                         // Handle the section header hiding/unhiding.
                         updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
 
                         // Update the "Tabs from last time" part of the tab list.
                         updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
                     }
                 });
             }
-        });
+        }, "LastSessionTabsThread");
+
+        parseThread.start();
     }
 
     public void clearLastSessionData() {
         final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0];
 
         // Only modify mLastSessionTabs on the UI thread.
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
--- a/toolkit/components/downloads/ApplicationReputation.cpp
+++ b/toolkit/components/downloads/ApplicationReputation.cpp
@@ -1023,16 +1023,18 @@ PendingLookup::SendRemoteQueryInternal()
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PendingLookup::Notify(nsITimer* aTimer)
 {
   LOG(("Remote lookup timed out [this = %p]", this));
   MOZ_ASSERT(aTimer == mTimeoutTimer);
+  Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+    true);
   mChannel->Cancel(NS_ERROR_NET_TIMEOUT);
   mTimeoutTimer->Cancel();
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIStreamListener
 static NS_METHOD
@@ -1067,16 +1069,19 @@ PendingLookup::OnStartRequest(nsIRequest
 NS_IMETHODIMP
 PendingLookup::OnStopRequest(nsIRequest *aRequest,
                              nsISupports *aContext,
                              nsresult aResult) {
   NS_ENSURE_STATE(mCallback);
 
   bool shouldBlock = false;
   uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE;
+  Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+    false);
+
   nsresult rv = OnStopRequestInternal(aRequest, aContext, aResult,
                                       &shouldBlock, &verdict);
   OnComplete(shouldBlock, rv, verdict);
   return rv;
 }
 
 nsresult
 PendingLookup::OnStopRequestInternal(nsIRequest *aRequest,
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -379,16 +379,17 @@ class ExtensionContext extends BaseConte
         addonId: attrs.addonId,
       };
 
       this.sandbox = Cu.Sandbox(prin, {
         metadata,
         sandboxPrototype: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
+        wantExportHelpers: true,
         wantGlobalProperties: ["XMLHttpRequest", "fetch"],
       });
 
       Cu.evalInSandbox(`
         window.JSON = JSON;
         window.XMLHttpRequest = XMLHttpRequest;
         window.fetch = fetch;
       `, this.sandbox);
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 support-files =
+  chrome_head.js
   head.js
   file_download.html
   file_download.txt
   interruptible.sjs
   file_sample.html
 
 [test_chrome_ext_background_debug_global.html]
 skip-if = (os == 'android') # android doesn't have devtools
@@ -13,16 +14,17 @@ skip-if = (toolkit == 'android') # andro
 [test_chrome_ext_downloads_download.html]
 [test_chrome_ext_downloads_misc.html]
 [test_chrome_ext_downloads_search.html]
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_native_messaging.html]
 skip-if = os == "android"  # native messaging is not supported on android
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
+[test_chrome_ext_trustworthy_origin.html]
 [test_chrome_ext_webnavigation_resolved_urls.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_native_messaging_paths.html]
 skip-if = os != "mac" && os != "linux"
 [test_ext_cookies_expiry.html]
 skip-if = buildapp == 'b2g'
 [test_ext_cookies_permissions.html]
 skip-if = buildapp == 'b2g'
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome_head.js
@@ -0,0 +1,12 @@
+"use strict";
+
+const {
+  classes: Cc,
+  interfaces: Ci,
+  utils: Cu,
+  results: Cr,
+} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -43,16 +43,17 @@ skip-if = os == 'android' # Android does
 [test_ext_geturl.html]
 [test_ext_background_canvas.html]
 [test_ext_content_security_policy.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
 [test_ext_contentscript_api_injection.html]
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_devtools_metadata.html]
+[test_ext_contentscript_exporthelpers.html]
 [test_ext_contentscript_css.html]
 [test_ext_downloads.html]
 [test_ext_exclude_include_globs.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_idle.html]
 [test_ext_localStorage.html]
 [test_ext_onmessage_removelistener.html]
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -1,25 +1,23 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
-const {
-  utils: Cu,
-} = Components;
 
 Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm");
 
 /**
  * This test is asserting that ext-backgroundPage.js successfully sets its
  * debug global in the AddonWrapper provided by XPIProvider.jsm
  *
  * It does _not_ test any functionality in devtools and does not guarantee
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
@@ -1,26 +1,24 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 
 /* eslint-disable mozilla/balanced-listeners */
 
 add_task(function* testAlertNotShownInBackgroundWindow() {
   ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(),
      "Alerts should not be present at the start of the test.");
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for content script unrecognized property on manifest</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_download.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_download.html
@@ -1,35 +1,30 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-const {
-  interfaces: Ci,
-  utils: Cu,
-} = Components;
-
 /* global OS */
 
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Downloads.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
 const WINDOWS = (AppConstants.platform == "win");
 
 const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
 const FILE_NAME = "file_download.txt";
 const FILE_URL = BASE + "/" + FILE_NAME;
 const FILE_NAME_UNIQUE = "file_download(1).txt";
 const FILE_LEN = 46;
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_misc.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_misc.html
@@ -1,29 +1,24 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-const {
-  interfaces: Ci,
-  utils: Cu,
-} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Downloads.jsm");
 
 const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
 const TXT_FILE = "file_download.txt";
 const TXT_URL = BASE + "/" + TXT_FILE;
 
 // Keep these in sync with code in interruptible.sjs
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_search.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_search.html
@@ -1,29 +1,24 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-const {
-  interfaces: Ci,
-  utils: Cu,
-} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Downloads.jsm");
 
 const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
 const TXT_FILE = "file_download.txt";
 const TXT_URL = BASE + "/" + TXT_FILE;
 const TXT_LEN = 46;
 const HTML_FILE = "file_download.html";
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for WebExtension EventPage Warning</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
@@ -1,31 +1,29 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 /* globals OS */
 
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 
 if (AppConstants.platform == "win") {
   Cu.import("resource://testing-common/MockRegistry.jsm");
 }
 
 var promiseConsoleOutput = Task.async(function* (task) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>WebExtension test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * This test is asserting that moz-extension: URLs are recognized as trustworthy local origins
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+                                   "@mozilla.org/contentsecuritymanager;1",
+                                   "nsIContentSecurityManager");
+
+add_task(function* () {
+  function backgroundScript() {
+    browser.test.sendMessage("ready", browser.runtime.getURL("/test.html"));
+  }
+
+  let extensionData = {
+    background: "(" + backgroundScript.toString() + ")()",
+    manifest: {},
+    files: {
+      "test.html": `<html><head></head><body></body></html>`,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+
+  let url = yield extension.awaitMessage("ready");
+
+  let uri = NetUtil.newURI(url);
+  let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
+  is(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), true);
+
+  yield extension.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for simple WebExtension</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
@@ -1,30 +1,28 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
 /* global OS */
 
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
 // Test that the default paths searched for native host manifests
 // are the ones we expect.
 add_task(function* test_default_paths() {
   let expectUser, expectGlobal;
   switch (AppConstants.platform) {
     case "macosx": {
       expectUser = OS.Path.join(OS.Constants.Path.homeDir,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for content script</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_contentscript_exportHelpers() {
+  function contentScript() {
+    browser.test.assertTrue(typeof cloneInto === "function");
+    browser.test.assertTrue(typeof createObjectIn === "function");
+    browser.test.assertTrue(typeof exportFunction === "function");
+
+    /* globals exportFunction, precisePi, reportPi */
+    let value = 3.14;
+    exportFunction(() => value, window, {defineAs: "precisePi"});
+
+    browser.test.assertEq("undefined", typeof precisePi,
+        "exportFunction should export to the page's scope only");
+
+    browser.test.assertEq("undefined", typeof window.precisePi,
+        "exportFunction should export to the page's scope only");
+
+    let results = [];
+    exportFunction(pi => results.push(pi), window, {defineAs: "reportPi"});
+
+    let s = document.createElement("script");
+    s.textContent = `(${function() {
+      let result1 = "unknown 1";
+      let result2 = "unknown 2";
+      try {
+        result1 = precisePi();
+      } catch (e) {
+        result1 = "err:" + e;
+      }
+      try {
+        result2 = window.precisePi();
+      } catch (e) {
+        result2 = "err:" + e;
+      }
+      reportPi(result1);
+      reportPi(result2);
+    }})();`;
+
+    document.documentElement.appendChild(s);
+    // Inline script ought to run synchronously.
+
+    browser.test.assertEq(3.14, results[0],
+        "exportFunction on window should define a global function");
+    browser.test.assertEq(3.14, results[1],
+        "exportFunction on window should export a property to window.");
+
+    browser.test.assertEq(2, results.length,
+        "Expecting the number of results to match the number of method calls");
+
+    browser.test.notifyPass("export helper test completed");
+  }
+
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["contentscript.js"],
+        matches: ["http://mochi.test/*/file_sample.html"],
+        run_at: "document_start",
+      }],
+    },
+
+    files: {
+      "contentscript.js": `(${contentScript})();`,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+
+  yield extension.awaitFinish("export helper test completed");
+  win.close();
+
+  yield extension.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>WebExtension test</title>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
@@ -1,16 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for simple WebExtension</title>
   <meta charset="utf-8">
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for schema API creation</title>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -83,16 +83,23 @@
     "n_values": 8,
     "description": "Application reputation remote response (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST)"
   },
   "APPLICATION_REPUTATION_COUNT": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Application reputation query count (both local and remote)"
   },
+  "APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT": {
+    "alert_emails": ["gcp@mozilla.com", "francois@mozilla.com"],
+    "expires_in_version": "55",
+    "kind": "boolean",
+    "bug_numbers": [1172689],
+    "description": "Recorded when application reputation remote lookup is performed, `true` is recorded if the lookup times out."
+  },
   "AUDIOSTREAM_FIRST_OPEN_MS": {
     "expires_in_version": "50",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "The length of time (in milliseconds) for the first open of AudioStream."
   },
   "AUDIOSTREAM_LATER_OPEN_MS": {