Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 01 Jul 2016 11:18:56 +0200
changeset 343439 0a0baf81a9a7269455f77bb22b70207f9597abb7
parent 343438 56715a33b016d05afc2541e19fd7e0c907258971 (current diff)
parent 343375 fdcee57b4e4f66a82831ab01e61500da98a858e8 (diff)
child 343440 95006e936e445294d749d1e1979648bb4ef0e8a1
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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 mozilla-central to mozilla-inbound
--- a/.eslintignore
+++ b/.eslintignore
@@ -108,16 +108,17 @@ devtools/client/webconsole/**
 !devtools/client/webconsole/console-commands.js
 devtools/client/webide/**
 !devtools/client/webide/components/webideCli.js
 devtools/server/**
 !devtools/server/css-logic.js
 !devtools/server/actors/webbrowser.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
+!devtools/server/actors/csscoverage.js
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
 !devtools/shared/event-emitter.js
 !devtools/shared/task.js
 devtools/shared/*.jsm
 !devtools/shared/Loader.jsm
 devtools/shared/apps/**
--- a/devtools/client/commandline/test/browser_cmd_screenshot.js
+++ b/devtools/client/commandline/test/browser_cmd_screenshot.js
@@ -94,17 +94,16 @@ function* addTabWithToolbarRunTests(win)
   yield helpers.audit(options, [
     {
       setup: "screenshot " + file.path,
       check: {
         args: {
           filename: { value: "" + file.path },
           fullpage: { value: false },
           clipboard: { value: false },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Saved to "),
       },
       post: function () {
         // Bug 849168: screenshot command tests fail in try but not locally
         // ok(file.exists(), "Screenshot file exists");
@@ -118,17 +117,16 @@ function* addTabWithToolbarRunTests(win)
 
   // Test capture to clipboard
   yield helpers.audit(options, [
     {
       setup: "screenshot --clipboard",
       check: {
         args: {
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         yield ContentTask.spawn(browser, imgSize, function* (imgSize) {
@@ -138,17 +136,16 @@ function* addTabWithToolbarRunTests(win)
       })
     },
     {
       setup: "screenshot --fullpage --clipboard",
       check: {
         args: {
           fullpage: { value: true },
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         yield ContentTask.spawn(browser, imgSize, function* (imgSize) {
@@ -161,17 +158,16 @@ function* addTabWithToolbarRunTests(win)
         });
       })
     },
     {
       setup: "screenshot --selector img#testImage --clipboard",
       check: {
         args: {
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         yield ContentTask.spawn(browser, imgSize, function* (imgSize) {
@@ -210,17 +206,16 @@ function* addTabWithToolbarRunTests(win)
 
   // Test capture to clipboard in presence of scrollbars
   yield helpers.audit(options, [
     {
       setup: "screenshot --clipboard",
       check: {
         args: {
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         imgSize.scrollbarWidth = scrollbarSize.width;
@@ -234,17 +229,16 @@ function* addTabWithToolbarRunTests(win)
       })
     },
     {
       setup: "screenshot --fullpage --clipboard",
       check: {
         args: {
           fullpage: { value: true },
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         imgSize.scrollbarWidth = scrollbarSize.width;
@@ -259,17 +253,16 @@ function* addTabWithToolbarRunTests(win)
         });
       })
     },
     {
       setup: "screenshot --selector img#testImage --clipboard",
       check: {
         args: {
           clipboard: { value: true },
-          chrome: { value: false },
         },
       },
       exec: {
         output: new RegExp("^Copied to clipboard.$"),
       },
       post: Task.async(function* () {
         let imgSize = yield getImageSizeFromClipboard();
         yield ContentTask.spawn(browser, imgSize, function* (imgSize) {
--- a/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
+++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js
@@ -14,29 +14,26 @@ add_task(function* () {
 
   yield selectNode("#testid", inspector);
   yield addNewRuleFromContextMenu(inspector, view);
   yield testNewRule(view);
 });
 
 function* addNewRuleFromContextMenu(inspector, view) {
   info("Waiting for context menu to be shown");
-  let onPopup = once(view._contextmenu._menupopup, "popupshown");
-  let win = view.styleWindow;
 
-  EventUtils.synthesizeMouseAtCenter(view.element,
-    {button: 2, type: "contextmenu"}, win);
-  yield onPopup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element);
+  let menuitemAddRule = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.addNewRule"));
 
-  ok(!view._contextmenu.menuitemAddRule.hidden, "Add rule is visible");
+  ok(menuitemAddRule.visible, "Add rule is visible");
 
   info("Adding the new rule and expecting a ruleview-changed event");
   let onRuleViewChanged = view.once("ruleview-changed");
-  view._contextmenu.menuitemAddRule.click();
-  view._contextmenu._menupopup.hidePopup();
+  menuitemAddRule.click();
   yield onRuleViewChanged;
 }
 
 function* testNewRule(view) {
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let editor = ruleEditor.selectorText.ownerDocument.activeElement;
   is(editor.value, "#testid", "Selector editor value is as expected");
 
--- a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js
@@ -53,21 +53,22 @@ add_task(function* () {
  *   depending on popupNode
  */
 function* testMdnContextMenuItemVisibility(view) {
   info("Test that MDN context menu item is shown only when it should be.");
 
   let root = rootElement(view);
   for (let node of iterateNodes(root)) {
     info("Setting " + node + " as popupNode");
-    view.styleDocument.popupNode = node;
+    info("Creating context menu with " + node + " as popupNode");
+    let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+    let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+      _STRINGS.GetStringFromName("styleinspector.contextmenu.showMdnDocs"));
 
-    info("Updating context menu state");
-    view._contextmenu._updateMenuItems();
-    let isVisible = !view._contextmenu.menuitemShowMdnDocs.hidden;
+    let isVisible = menuitemShowMdnDocs.visible;
     let shouldBeVisible = isPropertyNameNode(node);
     let message = shouldBeVisible ? "shown" : "hidden";
     is(isVisible, shouldBeVisible,
        "The MDN context menu item is " + message + " ; content : " +
        node.textContent + " ; type : " + node.nodeType);
   }
 }
 
--- a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
@@ -41,24 +41,25 @@ add_task(function* () {
 
 function* testShowMdnTooltip(view) {
   setBaseCssDocsUrl(URL_ROOT);
 
   info("Setting the popupNode for the MDN docs tooltip");
 
   let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
 
-  view.styleDocument.popupNode = nameSpan.firstChild;
-  view._contextmenu._updateMenuItems();
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild);
+  let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.showMdnDocs"));
 
   let cssDocs = view.tooltips.cssDocs;
 
   info("Showing the MDN docs tooltip");
   let onShown = cssDocs.tooltip.once("shown");
-  view._contextmenu.menuitemShowMdnDocs.click();
+  menuitemShowMdnDocs.click();
   yield onShown;
   ok(true, "The MDN docs tooltip was shown");
 }
 
 /**
  * Test that:
  *  - the MDN tooltip is shown when we click the context menu item
  *  - the tooltip's contents have been initialized (we don't fully
--- a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js
@@ -98,21 +98,21 @@ function* setBooleanPref(pref, state) {
  */
 function* testMdnContextMenuItemVisibility(view, shouldBeVisible) {
   let message = shouldBeVisible ? "shown" : "hidden";
   info("Test that MDN context menu item is " + message);
 
   info("Set a CSS property name as popupNode");
   let root = rootElement(view);
   let node = root.querySelector("." + PROPERTY_NAME_CLASS).firstChild;
-  view.styleDocument.popupNode = node;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+  let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.showMdnDocs"));
 
-  info("Update context menu state");
-  view._contextmenu._updateMenuItems();
-  let isVisible = !view._contextmenu.menuitemShowMdnDocs.hidden;
+  let isVisible = menuitemShowMdnDocs.visible;
   is(isVisible, shouldBeVisible,
      "The MDN context menu item is " + message);
 }
 
 /**
  * Returns the root element for the rule view.
  */
 var rootElement = view => (view.element) ? view.element : view.styleDocument;
--- a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -13,262 +13,272 @@ XPCOMUtils.defineLazyGetter(this, "osStr
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
 
 const TEST_URI = URL_ROOT + "doc_copystyles.html";
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let { inspector, view } = yield openRuleView();
-  let contextmenu = view._contextmenu;
   yield selectNode("#testid", inspector);
 
   let ruleEditor = getRuleViewRuleEditor(view, 1);
 
   let data = [
     {
       desc: "Test Copy Property Name",
       node: ruleEditor.rule.textProps[0].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyPropertyName,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyName",
       expectedPattern: "color",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Property Value",
       node: ruleEditor.rule.textProps[2].editor.valueSpan,
-      menuItem: contextmenu.menuitemCopyPropertyValue,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
       expectedPattern: "12px",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: true,
-        copyPropertyValue: false,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: false,
+        copyPropertyValue: true,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Property Value with Priority",
       node: ruleEditor.rule.textProps[3].editor.valueSpan,
-      menuItem: contextmenu.menuitemCopyPropertyValue,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyValue",
       expectedPattern: "#00F !important",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: true,
-        copyPropertyValue: false,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: false,
+        copyPropertyValue: true,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Property Declaration",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyPropertyDeclaration,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
       expectedPattern: "font-size: 12px;",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Property Declaration with Priority",
       node: ruleEditor.rule.textProps[3].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyPropertyDeclaration,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
       expectedPattern: "border-color: #00F !important;",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Rule",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyRule,
+      menuItemLabel: "styleinspector.contextmenu.copyRule",
       expectedPattern: "#testid {[\\r\\n]+" +
                        "\tcolor: #F00;[\\r\\n]+" +
                        "\tbackground-color: #00F;[\\r\\n]+" +
                        "\tfont-size: 12px;[\\r\\n]+" +
                        "\tborder-color: #00F !important;[\\r\\n]+" +
                        "\t--var: \"\\*/\";[\\r\\n]+" +
                        "}",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Selector",
       node: ruleEditor.selectorText,
-      menuItem: contextmenu.menuitemCopySelector,
+      menuItemLabel: "styleinspector.contextmenu.copySelector",
       expectedPattern: "html, body, #testid",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: true,
-        copyPropertyName: true,
-        copyPropertyValue: true,
-        copySelector: false,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: false,
+        copyPropertyName: false,
+        copyPropertyValue: false,
+        copySelector: true,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Location",
       node: ruleEditor.source,
-      menuItem: contextmenu.menuitemCopyLocation,
+      menuItemLabel: "styleinspector.contextmenu.copyLocation",
       expectedPattern: "http://example.com/browser/devtools/client/" +
                        "inspector/rules/test/doc_copystyles.css",
-      hidden: {
-        copyLocation: false,
-        copyPropertyDeclaration: true,
-        copyPropertyName: true,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: true,
+        copyPropertyDeclaration: false,
+        copyPropertyName: false,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       setup: function* () {
         yield disableProperty(view, 0);
       },
       desc: "Test Copy Rule with Disabled Property",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyRule,
+      menuItemLabel: "styleinspector.contextmenu.copyRule",
       expectedPattern: "#testid {[\\r\\n]+" +
                        "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
                        "\tbackground-color: #00F;[\\r\\n]+" +
                        "\tfont-size: 12px;[\\r\\n]+" +
                        "\tborder-color: #00F !important;[\\r\\n]+" +
                        "\t--var: \"\\*/\";[\\r\\n]+" +
                        "}",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       setup: function* () {
         yield disableProperty(view, 4);
       },
       desc: "Test Copy Rule with Disabled Property with Comment",
       node: ruleEditor.rule.textProps[2].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyRule,
+      menuItemLabel: "styleinspector.contextmenu.copyRule",
       expectedPattern: "#testid {[\\r\\n]+" +
                        "\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
                        "\tbackground-color: #00F;[\\r\\n]+" +
                        "\tfont-size: 12px;[\\r\\n]+" +
                        "\tborder-color: #00F !important;[\\r\\n]+" +
                        "\t/\\* --var: \"\\*\\\\\/\"; \\*\/[\\r\\n]+" +
                        "}",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
     {
       desc: "Test Copy Property Declaration with Disabled Property",
       node: ruleEditor.rule.textProps[0].editor.nameSpan,
-      menuItem: contextmenu.menuitemCopyPropertyDeclaration,
+      menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration",
       expectedPattern: "\/\\* color: #F00; \\*\/",
-      hidden: {
-        copyLocation: true,
-        copyPropertyDeclaration: false,
-        copyPropertyName: false,
-        copyPropertyValue: true,
-        copySelector: true,
-        copyRule: false
+      visible: {
+        copyLocation: false,
+        copyPropertyDeclaration: true,
+        copyPropertyName: true,
+        copyPropertyValue: false,
+        copySelector: false,
+        copyRule: true
       }
     },
   ];
 
-  for (let { setup, desc, node, menuItem, expectedPattern, hidden } of data) {
+  for (let { setup, desc, node, menuItemLabel, expectedPattern, visible } of data) {
     if (setup) {
       yield setup();
     }
 
     info(desc);
-    yield checkCopyStyle(view, node, menuItem, expectedPattern, hidden);
+    yield checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible);
   }
 });
 
-function* checkCopyStyle(view, node, menuItem, expectedPattern, hidden) {
-  let onPopup = once(view._contextmenu._menupopup, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(node,
-    {button: 2, type: "contextmenu"}, view.styleWindow);
-  yield onPopup;
+function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) {
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, node);
+  let menuItem = allMenuItems.find(item =>
+    item.label === _STRINGS.GetStringFromName(menuItemLabel));
+  let menuitemCopy = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copy"));
+  let menuitemCopyLocation = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyLocation"));
+  let menuitemCopyPropertyDeclaration = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyPropertyDeclaration"));
+  let menuitemCopyPropertyName = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyPropertyName"));
+  let menuitemCopyPropertyValue = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyPropertyValue"));
+  let menuitemCopySelector = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copySelector"));
+  let menuitemCopyRule = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyRule"));
 
-  ok(view._contextmenu.menuitemCopy.disabled,
+  ok(menuitemCopy.disabled,
     "Copy disabled is as expected: true");
-  ok(!view._contextmenu.menuitemCopy.hidden,
-    "Copy hidden is as expected: false");
-
-  is(view._contextmenu.menuitemCopyLocation.hidden,
-     hidden.copyLocation,
-     "Copy Location hidden attribute is as expected: " +
-     hidden.copyLocation);
+  ok(menuitemCopy.visible,
+    "Copy visible is as expected: true");
 
-  is(view._contextmenu.menuitemCopyPropertyDeclaration.hidden,
-     hidden.copyPropertyDeclaration,
-     "Copy Property Declaration hidden attribute is as expected: " +
-     hidden.copyPropertyDeclaration);
+  is(menuitemCopyLocation.visible,
+     visible.copyLocation,
+     "Copy Location visible attribute is as expected: " +
+     visible.copyLocation);
 
-  is(view._contextmenu.menuitemCopyPropertyName.hidden,
-     hidden.copyPropertyName,
-     "Copy Property Name hidden attribute is as expected: " +
-     hidden.copyPropertyName);
+  is(menuitemCopyPropertyDeclaration.visible,
+     visible.copyPropertyDeclaration,
+     "Copy Property Declaration visible attribute is as expected: " +
+     visible.copyPropertyDeclaration);
+
+  is(menuitemCopyPropertyName.visible,
+     visible.copyPropertyName,
+     "Copy Property Name visible attribute is as expected: " +
+     visible.copyPropertyName);
 
-  is(view._contextmenu.menuitemCopyPropertyValue.hidden,
-     hidden.copyPropertyValue,
-     "Copy Property Value hidden attribute is as expected: " +
-     hidden.copyPropertyValue);
+  is(menuitemCopyPropertyValue.visible,
+     visible.copyPropertyValue,
+     "Copy Property Value visible attribute is as expected: " +
+     visible.copyPropertyValue);
 
-  is(view._contextmenu.menuitemCopySelector.hidden,
-     hidden.copySelector,
-     "Copy Selector hidden attribute is as expected: " +
-     hidden.copySelector);
+  is(menuitemCopySelector.visible,
+     visible.copySelector,
+     "Copy Selector visible attribute is as expected: " +
+     visible.copySelector);
 
-  is(view._contextmenu.menuitemCopyRule.hidden,
-     hidden.copyRule,
-     "Copy Rule hidden attribute is as expected: " +
-     hidden.copyRule);
+  is(menuitemCopyRule.visible,
+     visible.copyRule,
+     "Copy Rule visible attribute is as expected: " +
+     visible.copyRule);
 
   try {
     yield waitForClipboard(() => menuItem.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
   }
-
-  view._contextmenu._menupopup.hidePopup();
 }
 
 function* disableProperty(view, index) {
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let textProp = ruleEditor.rule.textProps[index];
   yield togglePropStatus(view, textProp);
 }
 
--- a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -64,108 +64,91 @@ function* checkCopySelection(view) {
   let expectedPattern = "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica, sans-serif;[\\r\\n]+" +
                         "    color: #AAA;[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
                         "    color: #000000;[\\r\\n]*";
 
-  let onPopup = once(view._contextmenu._menupopup, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(prop,
-    {button: 2, type: "contextmenu"}, win);
-  yield onPopup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+  let menuitemCopy = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copy"));
 
-  ok(!view._contextmenu.menuitemCopy.hidden,
+  ok(menuitemCopy.visible,
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => view._contextmenu.menuitemCopy.click(),
+    yield waitForClipboard(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
   }
-
-  view._contextmenu._menupopup.hidePopup();
 }
 
 function* checkSelectAll(view) {
   info("Testing select-all copy");
 
   let contentDoc = view.styleDocument;
-  let win = view.styleWindow;
   let prop = contentDoc.querySelector(".ruleview-property");
 
   info("Checking that _SelectAll() then copy returns the correct " +
     "clipboard value");
   view._contextmenu._onSelectAll();
   let expectedPattern = "element {[\\r\\n]+" +
                         "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica, sans-serif;[\\r\\n]+" +
                         "    color: #AAA;[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
                         "    color: #000000;[\\r\\n]+" +
                         "}[\\r\\n]*";
 
-  let onPopup = once(view._contextmenu._menupopup, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(prop,
-    {button: 2, type: "contextmenu"}, win);
-  yield onPopup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop);
+  let menuitemCopy = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copy"));
 
-  ok(!view._contextmenu.menuitemCopy.hidden,
+  ok(menuitemCopy.visible,
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => view._contextmenu.menuitemCopy.click(),
+    yield waitForClipboard(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
   }
-
-  view._contextmenu._menupopup.hidePopup();
 }
 
 function* checkCopyEditorValue(view) {
   info("Testing CSS property editor value copy");
 
-  let win = view.styleWindow;
   let ruleEditor = getRuleViewRuleEditor(view, 0);
   let propEditor = ruleEditor.rule.textProps[0].editor;
 
   let editor = yield focusEditableField(view, propEditor.valueSpan);
 
   info("Checking that copying a css property value editor returns the correct" +
     " clipboard value");
 
   let expectedPattern = "10em";
 
-  let onPopup = once(view._contextmenu._menupopup, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(editor.input,
-    {button: 2, type: "contextmenu"}, win);
-  yield onPopup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input);
+  let menuitemCopy = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copy"));
 
-  ok(!view._contextmenu.menuitemCopy.hidden,
+  ok(menuitemCopy.visible,
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => view._contextmenu.menuitemCopy.click(),
+    yield waitForClipboard(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
   }
-
-  view._contextmenu._menupopup.hidePopup();
-
-  // The value field is still focused. Blur it now and wait for the
-  // ruleview-changed event to avoid pending requests.
-  let onRuleViewChanged = view.once("ruleview-changed");
-  EventUtils.synthesizeKey("VK_ESCAPE", {});
-  yield onRuleViewChanged;
 }
 
 function checkClipboardData(expectedPattern) {
   let actual = SpecialPowers.getClipboardData("text/unicode");
   let expectedRegExp = new RegExp(expectedPattern, "g");
   return expectedRegExp.test(actual);
 }
 
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -14,16 +14,18 @@ registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
 });
 
 var {getInplaceEditorForSpan: inplaceEditor} =
   require("devtools/client/shared/inplace-editor");
 
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+const _STRINGS = Services.strings.createBundle(
+  "chrome://devtools-shared/locale/styleinspector.properties");
 
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
 });
 
 /**
  * The rule-view tests rely on a frame-script to be injected in the content test
  * page. So override the shared-head's addTab to load the frame script after the
@@ -777,8 +779,28 @@ function* addNewRuleAndDismissEditor(ins
  */
 function* sendKeysAndWaitForFocus(view, element, keys) {
   let onFocus = once(element, "focus", true);
   for (let key of keys) {
     EventUtils.sendKey(key, view.styleWindow);
   }
   yield onFocus;
 }
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ *        The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+  let menu = view._contextmenu._openMenu({target: target});
+
+  // Flatten all menu items into a single array to make searching through it easier
+  let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+    if (item.submenu) {
+      return addItem(item.submenu.items);
+    }
+    return item;
+  }));
+
+  return allItems;
+}
--- a/devtools/client/inspector/shared/style-inspector-menu.js
+++ b/devtools/client/inspector/shared/style-inspector-menu.js
@@ -6,26 +6,28 @@
 /* global _strings */
 
 "use strict";
 
 const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
 const Services = require("Services");
 const {Task} = require("devtools/shared/task");
 
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
 loader.lazyRequireGetter(this, "overlays",
   "devtools/client/inspector/shared/style-inspector-overlays");
 loader.lazyServiceGetter(this, "clipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
 loader.lazyGetter(this, "_strings", () => {
   return Services.strings
   .createBundle("chrome://devtools-shared/locale/styleinspector.properties");
 });
 
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const PREF_ENABLE_MDN_DOCS_TOOLTIP =
   "devtools.inspector.mdnDocsTooltip.enabled";
 
 /**
  * Style inspector context menu
  *
  * @param {RuleView|ComputedView} view
  *        RuleView or ComputedView instance controlling this menu
@@ -49,232 +51,215 @@ function StyleInspectorMenu(view, option
   this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
   this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
   this._onCopyRule = this._onCopyRule.bind(this);
   this._onCopySelector = this._onCopySelector.bind(this);
   this._onCopyUrl = this._onCopyUrl.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
-  this._updateMenuItems = this._updateMenuItems.bind(this);
-
-  this._createContextMenu();
 }
 
 module.exports = StyleInspectorMenu;
 
 StyleInspectorMenu.prototype = {
   /**
    * Display the style inspector context menu
    */
   show: function (event) {
     try {
-      // In the sidebar we do not have this.styleDocument.popupNode
-      // so we need to save the node ourselves.
-      this.styleDocument.popupNode = event.explicitOriginalTarget;
-      this.styleWindow.focus();
-      this._menupopup.openPopupAtScreen(event.screenX, event.screenY, true);
+      this._openMenu({
+        target: event.explicitOriginalTarget,
+        screenX: event.screenX,
+        screenY: event.screenY,
+      });
     } catch (e) {
       console.error(e);
     }
   },
 
-  _createContextMenu: function () {
-    this._menupopup = this.styleDocument.createElementNS(XUL_NS, "menupopup");
-    this._menupopup.addEventListener("popupshowing", this._updateMenuItems);
-    this._menupopup.id = "computed-view-context-menu";
-
-    let parentDocument = this.styleWindow.parent.document;
-    let popupset = parentDocument.documentElement.querySelector("popupset");
-    if (!popupset) {
-      popupset = parentDocument.createElementNS(XUL_NS, "popupset");
-      parentDocument.documentElement.appendChild(popupset);
-    }
-    popupset.appendChild(this._menupopup);
-
-    this._createContextMenuItems();
-  },
-  /**
-   * Create all context menu items
-   */
-  _createContextMenuItems: function () {
-    this.menuitemCopy = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copy",
-      accesskey: "styleinspector.contextmenu.copy.accessKey",
-      command: this._onCopy
-    });
-
-    this.menuitemCopyLocation = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyLocation",
-      command: this._onCopyLocation
-    });
-
-    this.menuitemCopyRule = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyRule",
-      command: this._onCopyRule
-    });
-
-    this.menuitemCopyColor = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyColor",
-      accesskey: "styleinspector.contextmenu.copyColor.accessKey",
-      command: this._onCopyColor
-    });
+  _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
+    // In the sidebar we do not have this.styleDocument.popupNode
+    // so we need to save the node ourselves.
+    this.styleDocument.popupNode = target;
+    this.styleWindow.focus();
 
-    this.menuitemCopyUrl = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyUrl",
-      accesskey: "styleinspector.contextmenu.copyUrl.accessKey",
-      command: this._onCopyUrl
-    });
-
-    this.menuitemCopyImageDataUrl = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyImageDataUrl",
-      accesskey: "styleinspector.contextmenu.copyImageDataUrl.accessKey",
-      command: this._onCopyImageDataUrl
-    });
-
-    this.menuitemCopyPropertyDeclaration = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyPropertyDeclaration",
-      command: this._onCopyPropertyDeclaration
-    });
-
-    this.menuitemCopyPropertyName = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyPropertyName",
-      command: this._onCopyPropertyName
-    });
-
-    this.menuitemCopyPropertyValue = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copyPropertyValue",
-      command: this._onCopyPropertyValue
-    });
-
-    this.menuitemCopySelector = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.copySelector",
-      command: this._onCopySelector
-    });
-
-    this._createMenuSeparator();
-
-    // Select All
-    this.menuitemSelectAll = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.selectAll",
-      accesskey: "styleinspector.contextmenu.selectAll.accessKey",
-      command: this._onSelectAll
-    });
-
-    this._createMenuSeparator();
+    let menu = new Menu();
 
-    // Add new rule
-    this.menuitemAddRule = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.addNewRule",
-      accesskey: "styleinspector.contextmenu.addNewRule.accessKey",
-      command: this._onAddNewRule
+    let menuitemCopy = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copy"),
+      accesskey: _strings.GetStringFromName("styleinspector.contextmenu.copy.accessKey"),
+      click: () => {
+        this._onCopy();
+      },
+      disabled: !this._hasTextSelected(),
+    });
+    let menuitemCopyLocation = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyLocation"),
+      click: () => {
+        this._onCopyLocation();
+      },
+      visible: false,
     });
-
-    // Show MDN Docs
-    this.menuitemShowMdnDocs = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.showMdnDocs",
-      accesskey: "styleinspector.contextmenu.showMdnDocs.accessKey",
-      command: this._onShowMdnDocs
+    let menuitemCopyRule = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyRule"),
+      click: () => {
+        this._onCopyRule();
+      },
+      visible: this.isRuleView,
     });
-
-    // Show Original Sources
-    this.menuitemSources = this._createContextMenuItem({
-      label: "styleinspector.contextmenu.toggleOrigSources",
-      accesskey: "styleinspector.contextmenu.toggleOrigSources.accessKey",
-      command: this._onToggleOrigSources,
-      type: "checkbox"
+    let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey";
+    let menuitemCopyColor = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyColor"),
+      accesskey: _strings.GetStringFromName(copyColorAccessKey),
+      click: () => {
+        this._onCopyColor();
+      },
+      visible: this._isColorPopup(),
     });
-  },
-
-  /**
-   * Create a single context menu item based on the provided configuration
-   * Returns the created menu item element
-   */
-  _createContextMenuItem: function (attributes) {
-    let ownerDocument = this._menupopup.ownerDocument;
-    let item = ownerDocument.createElementNS(XUL_NS, "menuitem");
-
-    item.setAttribute("label", _strings.GetStringFromName(attributes.label));
-    if (attributes.accesskey) {
-      item.setAttribute("accesskey",
-        _strings.GetStringFromName(attributes.accesskey));
-    }
-    item.addEventListener("command", attributes.command);
-
-    if (attributes.type) {
-      item.setAttribute("type", attributes.type);
-    }
-
-    this._menupopup.appendChild(item);
-    return item;
-  },
-
-  _createMenuSeparator: function () {
-    let ownerDocument = this._menupopup.ownerDocument;
-    let separator = ownerDocument.createElementNS(XUL_NS, "menuseparator");
-    this._menupopup.appendChild(separator);
-  },
-
-  /**
-   * Update the context menu. This means enabling or disabling menuitems as
-   * appropriate.
-   */
-  _updateMenuItems: function () {
-    this._updateCopyMenuItems();
-
-    let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
-    this.menuitemSources.setAttribute("checked", showOrig);
-
-    let enableMdnDocsTooltip =
-      Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
-    this.menuitemShowMdnDocs.hidden = !(enableMdnDocsTooltip &&
-                                        this._isPropertyName());
-
-    this.menuitemAddRule.hidden = !this.isRuleView;
-    this.menuitemAddRule.disabled = !this.isRuleView ||
-                                    this.inspector.selection.isAnonymousNode();
-  },
-
-  /**
-   * Display the necessary copy context menu items depending on the clicked
-   * node and selection in the rule view.
-   */
-  _updateCopyMenuItems: function () {
-    this.menuitemCopy.disabled = !this._hasTextSelected();
-
-    this.menuitemCopyColor.hidden = !this._isColorPopup();
-    this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl();
-    this.menuitemCopyUrl.hidden = !this._isImageUrl();
-    this.menuitemCopyRule.hidden = !this.isRuleView;
-
-    this.menuitemCopyLocation.hidden = true;
-    this.menuitemCopyPropertyDeclaration.hidden = true;
-    this.menuitemCopyPropertyName.hidden = true;
-    this.menuitemCopyPropertyValue.hidden = true;
-    this.menuitemCopySelector.hidden = true;
+    let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey";
+    let menuitemCopyUrl = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyUrl"),
+      accesskey: _strings.GetStringFromName(copyUrlAccessKey),
+      click: () => {
+        this._onCopyUrl();
+      },
+      visible: this._isImageUrl(),
+    });
+    let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey";
+    let menuitemCopyImageDataUrl = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyImageDataUrl"),
+      accesskey: _strings.GetStringFromName(copyImageAccessKey),
+      click: () => {
+        this._onCopyImageDataUrl();
+      },
+      visible: this._isImageUrl(),
+    });
+    let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration";
+    let menuitemCopyPropertyDeclaration = new MenuItem({
+      label: _strings.GetStringFromName(copyPropDeclarationLabel),
+      click: () => {
+        this._onCopyPropertyDeclaration();
+      },
+      visible: false,
+    });
+    let menuitemCopyPropertyName = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyPropertyName"),
+      click: () => {
+        this._onCopyPropertyName();
+      },
+      visible: false,
+    });
+    let menuitemCopyPropertyValue = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copyPropertyValue"),
+      click: () => {
+        this._onCopyPropertyValue();
+      },
+      visible: false,
+    });
+    let menuitemCopySelector = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.copySelector"),
+      click: () => {
+        this._onCopySelector();
+      },
+      visible: false,
+    });
 
     this._clickedNodeInfo = this._getClickedNodeInfo();
     if (this.isRuleView && this._clickedNodeInfo) {
       switch (this._clickedNodeInfo.type) {
         case overlays.VIEW_NODE_PROPERTY_TYPE :
-          this.menuitemCopyPropertyDeclaration.hidden = false;
-          this.menuitemCopyPropertyName.hidden = false;
+          menuitemCopyPropertyDeclaration.visible = true;
+          menuitemCopyPropertyName.visible = true;
           break;
         case overlays.VIEW_NODE_VALUE_TYPE :
-          this.menuitemCopyPropertyDeclaration.hidden = false;
-          this.menuitemCopyPropertyValue.hidden = false;
+          menuitemCopyPropertyDeclaration.visible = true;
+          menuitemCopyPropertyValue.visible = true;
           break;
         case overlays.VIEW_NODE_SELECTOR_TYPE :
-          this.menuitemCopySelector.hidden = false;
+          menuitemCopySelector.visible = true;
           break;
         case overlays.VIEW_NODE_LOCATION_TYPE :
-          this.menuitemCopyLocation.hidden = false;
+          menuitemCopyLocation.visible = true;
           break;
       }
     }
+
+    menu.append(menuitemCopy);
+    menu.append(menuitemCopyLocation);
+    menu.append(menuitemCopyRule);
+    menu.append(menuitemCopyColor);
+    menu.append(menuitemCopyUrl);
+    menu.append(menuitemCopyImageDataUrl);
+    menu.append(menuitemCopyPropertyDeclaration);
+    menu.append(menuitemCopyPropertyName);
+    menu.append(menuitemCopyPropertyValue);
+    menu.append(menuitemCopySelector);
+
+    menu.append(new MenuItem({
+      type: "separator",
+    }));
+
+    // Select All
+    let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey";
+    let menuitemSelectAll = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.selectAll"),
+      accesskey: _strings.GetStringFromName(selectAllAccessKey),
+      click: () => {
+        this._onSelectAll();
+      },
+    });
+    menu.append(menuitemSelectAll);
+
+    menu.append(new MenuItem({
+      type: "separator",
+    }));
+
+    // Add new rule
+    let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey";
+    let menuitemAddRule = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.addNewRule"),
+      accesskey: _strings.GetStringFromName(addRuleAccessKey),
+      click: () => {
+        this._onAddNewRule();
+      },
+      visible: this.isRuleView,
+      disabled: !this.isRuleView ||
+                this.inspector.selection.isAnonymousNode(),
+    });
+    menu.append(menuitemAddRule);
+
+    // Show MDN Docs
+    let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey";
+    let menuitemShowMdnDocs = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.showMdnDocs"),
+      accesskey: _strings.GetStringFromName(mdnDocsAccessKey),
+      click: () => {
+        this._onShowMdnDocs();
+      },
+      visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) &&
+                                                    this._isPropertyName()),
+    });
+    menu.append(menuitemShowMdnDocs);
+
+    // Show Original Sources
+    let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey";
+    let menuitemSources = new MenuItem({
+      label: _strings.GetStringFromName("styleinspector.contextmenu.toggleOrigSources"),
+      accesskey: _strings.GetStringFromName(sourcesAccessKey),
+      click: () => {
+        this._onToggleOrigSources();
+      },
+      type: "checkbox",
+      checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
+    });
+    menu.append(menuitemSources);
+
+    menu.popup(screenX, screenY, this.inspector._toolbox);
+    return menu;
   },
 
   _hasTextSelected: function () {
     let hasTextSelected;
     let selection = this.styleWindow.getSelection();
 
     let node = this._getClickedNode();
     if (node.nodeName == "input" || node.nodeName == "textarea") {
@@ -507,51 +492,16 @@ StyleInspectorMenu.prototype = {
    *  Toggle the original sources pref.
    */
   _onToggleOrigSources: function () {
     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
   destroy: function () {
-    this._removeContextMenuItems();
-
-    // Destroy the context menu.
-    this._menupopup.removeEventListener("popupshowing", this._updateMenuItems);
-    this._menupopup.parentNode.removeChild(this._menupopup);
-    this._menupopup = null;
-
     this.popupNode = null;
     this.styleDocument.popupNode = null;
     this.view = null;
     this.inspector = null;
     this.styleDocument = null;
     this.styleWindow = null;
-  },
-
-  _removeContextMenuItems: function () {
-    this._removeContextMenuItem("menuitemAddRule", this._onAddNewRule);
-    this._removeContextMenuItem("menuitemCopy", this._onCopy);
-    this._removeContextMenuItem("menuitemCopyColor", this._onCopyColor);
-    this._removeContextMenuItem("menuitemCopyImageDataUrl",
-      this._onCopyImageDataUrl);
-    this._removeContextMenuItem("menuitemCopyLocation", this._onCopyLocation);
-    this._removeContextMenuItem("menuitemCopyPropertyDeclaration",
-      this._onCopyPropertyDeclaration);
-    this._removeContextMenuItem("menuitemCopyPropertyName",
-      this._onCopyPropertyName);
-    this._removeContextMenuItem("menuitemCopyPropertyValue",
-      this._onCopyPropertyValue);
-    this._removeContextMenuItem("menuitemCopyRule", this._onCopyRule);
-    this._removeContextMenuItem("menuitemCopySelector", this._onCopySelector);
-    this._removeContextMenuItem("menuitemCopyUrl", this._onCopyUrl);
-    this._removeContextMenuItem("menuitemSelectAll", this._onSelectAll);
-    this._removeContextMenuItem("menuitemShowMdnDocs", this._onShowMdnDocs);
-    this._removeContextMenuItem("menuitemSources", this._onToggleOrigSources);
-  },
-
-  _removeContextMenuItem: function (itemName, callback) {
-    if (this[itemName]) {
-      this[itemName].removeEventListener("command", callback);
-      this[itemName] = null;
-    }
   }
 };
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -25,30 +25,29 @@ add_task(function* () {
   yield testColorPickerEdit(inspector, view);
 });
 
 function* testCopyToClipboard(inspector, view) {
   info("Testing that color is copied to clipboard");
 
   yield selectNode("div", inspector);
 
-  let win = view.styleWindow;
   let element = getRuleViewProperty(view, "div", "color").valueSpan
     .querySelector(".ruleview-colorswatch");
 
-  let popup = once(view._contextmenu._menupopup, "popupshown");
-  EventUtils.synthesizeMouseAtCenter(element, {button: 2, type: "contextmenu"},
-    win);
-  yield popup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, element);
+  let menuitemCopyColor = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyColor"));
 
-  ok(!view._contextmenu.menuitemCopyColor.hidden, "Copy color is visible");
+  ok(menuitemCopyColor.visible, "Copy color is visible");
 
-  yield waitForClipboard(() => view._contextmenu.menuitemCopyColor.click(),
+  yield waitForClipboard(() => menuitemCopyColor.click(),
     "#123ABC");
-  view._contextmenu._menupopup.hidePopup();
+
+  EventUtils.synthesizeKey("VK_ESCAPE", { });
 }
 
 function* testManualEdit(inspector, view) {
   info("Testing manually edited colors");
   yield selectNode("div", inspector);
 
   let {valueSpan} = getRuleViewProperty(view, "div", "color");
 
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
@@ -72,61 +72,42 @@ function* testCopyUrlToClipboard({view, 
 
   info("Retrieve background-image link for selected node in current " +
        "styleinspector view");
   let property = getBackgroundImageProperty(view, selector);
   let imageLink = property.valueSpan.querySelector(".theme-link");
   ok(imageLink, "Background-image link element found");
 
   info("Simulate right click on the background-image URL");
-  let popup = once(view._contextmenu._menupopup, "popupshown");
-
-  // Cannot rely on synthesizeMouseAtCenter here. The image URL can be displayed
-  // on several lines. A click simulated at the exact center may click between
-  // the lines and miss the target. Instead, using the top-left corner of first
-  // client rect, with an offset of 2 pixels.
-  let rect = imageLink.getClientRects()[0];
-  let x = rect.left + 2;
-  let y = rect.top + 2;
-
-  EventUtils.synthesizeMouseAtPoint(x, y, {
-    button: 2,
-    type: "contextmenu"
-  }, getViewWindow(view));
-  yield popup;
+  let allMenuItems = openStyleContextMenuAndGetAllItems(view, imageLink);
+  let menuitemCopyUrl = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyUrl"));
+  let menuitemCopyImageDataUrl = allMenuItems.find(item => item.label ===
+    _STRINGS.GetStringFromName("styleinspector.contextmenu.copyImageDataUrl"));
 
   info("Context menu is displayed");
-  ok(!view._contextmenu.menuitemCopyUrl.hidden,
+  ok(menuitemCopyUrl.visible,
      "\"Copy URL\" menu entry is displayed");
-  ok(!view._contextmenu.menuitemCopyImageDataUrl.hidden,
+  ok(menuitemCopyImageDataUrl.visible,
      "\"Copy Image Data-URL\" menu entry is displayed");
 
   if (type == "data-uri") {
     info("Click Copy Data URI and wait for clipboard");
     yield waitForClipboard(() => {
-      return view._contextmenu.menuitemCopyImageDataUrl.click();
+      return menuitemCopyImageDataUrl.click();
     }, expected);
   } else {
     info("Click Copy URL and wait for clipboard");
     yield waitForClipboard(() => {
-      return view._contextmenu.menuitemCopyUrl.click();
+      return menuitemCopyUrl.click();
     }, expected);
   }
 
   info("Hide context menu");
-  view._contextmenu._menupopup.hidePopup();
 }
 
 function getBackgroundImageProperty(view, selector) {
   let isRuleView = view instanceof CssRuleView;
   if (isRuleView) {
     return getRuleViewProperty(view, selector, "background-image");
   }
   return getComputedViewProperty(view, "background-image");
 }
-
-/**
- * Function that returns the window for a given view.
- */
-function getViewWindow(view) {
-  let viewDocument = view.styleDocument ? view.styleDocument : view.doc;
-  return viewDocument.defaultView;
-}
--- a/devtools/client/inspector/shared/test/head.js
+++ b/devtools/client/inspector/shared/test/head.js
@@ -16,16 +16,18 @@ var {getInplaceEditorForSpan: inplaceEdi
   require("devtools/client/shared/inplace-editor");
 
 const TEST_URL_ROOT =
   "http://example.com/browser/devtools/client/inspector/shared/test/";
 const TEST_URL_ROOT_SSL =
   "https://example.com/browser/devtools/client/inspector/shared/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
+const _STRINGS = Services.strings.createBundle(
+  "chrome://devtools-shared/locale/styleinspector.properties");
 
 // Clean-up all prefs that might have been changed during a test run
 // (safer here because if the test fails, then the pref is never reverted)
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
 });
 
 /**
@@ -529,8 +531,28 @@ function getComputedViewProperty(view, n
  * @param {String} name
  *        The name of the property to retrieve
  * @return {String} The property value
  */
 function getComputedViewPropertyValue(view, name, propertyName) {
   return getComputedViewProperty(view, name, propertyName)
     .valueSpan.textContent;
 }
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ *        The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+  let menu = view._contextmenu._openMenu({target: target});
+
+  // Flatten all menu items into a single array to make searching through it easier
+  let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+    if (item.submenu) {
+      return addItem(item.submenu.items);
+    }
+    return item;
+  }));
+
+  return allItems;
+}
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -5,16 +5,18 @@ support-files =
 [test_HSplitBox_01.html]
 [test_notification_box_01.html]
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_reps_attribute.html]
 [test_reps_date-time.html]
 [test_reps_function.html]
 [test_reps_grip.html]
+[test_reps_null.html]
+[test_reps_object-with-text.html]
 [test_reps_object-with-url.html]
 [test_reps_stylesheet.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
 [test_frame_01.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_null.html
@@ -0,0 +1,42 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Null rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - Null</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* () {
+  try {
+    let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+    let { Null } = browserRequire("devtools/client/shared/components/reps/null");
+
+    let gripStub = {
+      "type": "null"
+    };
+
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+    is(renderedRep.type, Null.rep, `Rep correctly selects ${Null.rep.displayName}`);
+
+    // Test rendering
+    const renderedComponent = renderComponent(Null.rep, { object: gripStub });
+    is(renderedComponent.textContent, "null", "Null rep has expected text content");
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
@@ -0,0 +1,52 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ObjectWithText rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - ObjectWithText</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* () {
+  try {
+    let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+    let { ObjectWithText } = browserRequire("devtools/client/shared/components/reps/object-with-text");
+
+    let gripStub = {
+      "type": "object",
+      "class": "CSSStyleRule",
+      "actor": "server1.conn3.obj273",
+      "extensible": true,
+      "frozen": false,
+      "sealed": false,
+      "ownPropertyLength": 0,
+      "preview": {
+        "kind": "ObjectWithText",
+        "text": ".Shadow"
+      }
+    };
+
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+    is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
+
+    // Test rendering
+    const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
+    is(renderedComponent.textContent, ".Shadow", "ObjectWithText rep has expected text content");
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/shared/key-shortcuts.js
+++ b/devtools/client/shared/key-shortcuts.js
@@ -117,17 +117,18 @@ KeyShortcuts.parseElectronKey = function
       } else {
         shortcut.ctrl = true;
       }
     } else if (["Control", "Ctrl"].includes(mod)) {
       shortcut.ctrl = true;
     } else if (mod === "Shift") {
       shortcut.shift = true;
     } else {
-      throw new Error("Unsupported modifier: " + mod);
+      console.error("Unsupported modifier:", mod, "from key:", str);
+      return null;
     }
   }
 
   // Plus is a special case. It's a character key and shouldn't be matched
   // against a keycode as it is only accessible via Shift/Capslock
   if (key === "Plus") {
     key = "+";
   }
@@ -137,17 +138,18 @@ KeyShortcuts.parseElectronKey = function
     shortcut.key = key.toLowerCase();
   } else if (key in ElectronKeysMapping) {
     // Maps the others manually to DOM API DOM_VK_*
     key = ElectronKeysMapping[key];
     shortcut.keyCode = window.KeyboardEvent[key];
     // Used only to stringify the shortcut
     shortcut.keyCodeString = key;
   } else {
-    throw new Error("Unsupported key: " + key);
+    console.error("Unsupported key:", key);
+    return null;
   }
 
   return shortcut;
 };
 
 KeyShortcuts.stringify = function (shortcut) {
   let list = [];
   if (shortcut.alt) {
@@ -215,16 +217,20 @@ KeyShortcuts.prototype = {
 
   on(key, listener) {
     if (typeof listener !== "function") {
       throw new Error("KeyShortcuts.on() expects a function as " +
                       "second argument");
     }
     if (!this.keys.has(key)) {
       let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
+      // The key string is wrong and we were unable to compute the key shortcut
+      if (!shortcut) {
+        return;
+      }
       this.keys.set(key, shortcut);
     }
     this.eventEmitter.on(key, listener);
   },
 
   off(key, listener) {
     this.eventEmitter.off(key, listener);
   },
--- a/devtools/client/shared/test/browser_key_shortcuts.js
+++ b/devtools/client/shared/test/browser_key_shortcuts.js
@@ -13,16 +13,17 @@ add_task(function* () {
   yield testMixup(shortcuts);
   yield testLooseDigits(shortcuts);
   yield testExactModifiers(shortcuts);
   yield testLooseShiftModifier(shortcuts);
   yield testStrictLetterShiftModifier(shortcuts);
   yield testAltModifier(shortcuts);
   yield testCommandOrControlModifier(shortcuts);
   yield testCtrlModifier(shortcuts);
+  yield testInvalidShortcutString(shortcuts);
   shortcuts.destroy();
 
   yield testTarget();
 });
 
 // Test helper to listen to the next key press for a given key,
 // returning a promise to help using Tasks.
 function once(shortcuts, key, listener) {
@@ -357,8 +358,18 @@ function testTarget() {
   });
   EventUtils.synthesizeKey("0", {}, window);
   yield onKey;
 
   target.remove();
 
   shortcuts.destroy();
 }
+
+function testInvalidShortcutString(shortcuts) {
+  info("Test wrong shortcut string");
+
+  let shortcut = KeyShortcuts.parseElectronKey(window, "Cmmd+F");
+  ok(!shortcut, "Passing a invalid shortcut string should return a null object");
+
+  shortcuts.on("Cmmd+F", function () {});
+  ok(true, "on() shouldn't throw when passing invalid shortcut string");
+}
--- a/devtools/client/shared/widgets/filter-widget.css
+++ b/devtools/client/shared/widgets/filter-widget.css
@@ -195,17 +195,17 @@ input {
   display: block;
   order: 3;
   color: var(--theme-body-color-alt);
 }
 
 .remove-button {
   width: 16px;
   height: 16px;
-  background: url(chrome://devtools/skin/images/close@2x.png);
+  background: url(chrome://devtools/skin/images/close.svg);
   background-size: cover;
   font-size: 0;
   border: none;
   cursor: pointer;
 }
 
 .hidden {
   display: none !important;
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -611,29 +611,29 @@ StyleEditorUI.prototype = {
           this.emit("editor-selected", showEditor);
 
           // Is there any CSS coverage markup to include?
           let usage = yield csscoverage.getUsage(this._target);
           if (usage == null) {
             return;
           }
 
-          let href = csscoverage.sheetToUrl(showEditor.styleSheet);
-          let reportData = yield usage.createEditorReport(href);
+          let sheet = showEditor.styleSheet;
+          let {reports} = yield usage.createEditorReportForSheet(sheet);
 
           showEditor.removeAllUnusedRegions();
 
-          if (reportData.reports.length > 0) {
+          if (reports.length > 0) {
             // Only apply if this file isn't compressed. We detect a
             // compressed file if there are more rules than lines.
             let editorText = showEditor.sourceEditor.getText();
             let lineCount = editorText.split("\n").length;
             let ruleCount = showEditor.styleSheet.ruleCount;
             if (lineCount >= ruleCount) {
-              showEditor.addUnusedRegions(reportData.reports);
+              showEditor.addUnusedRegions(reports);
             } else {
               this.emit("error", { key: "error-compressed", level: "info" });
             }
           }
         }.bind(this)).then(null, e => console.error(e));
       }.bind(this)
     });
   },
--- a/devtools/client/themes/storage.css
+++ b/devtools/client/themes/storage.css
@@ -29,17 +29,18 @@
 
 #storage-sidebar {
   max-width: 500px;
   min-width: 250px;
 }
 
 /* Responsive sidebar */
 @media (max-width: 700px) {
-  #storage-tree {
+  #storage-tree,
+  #storage-sidebar {
     max-width: 100%;
   }
 
   #storage-table #path {
     display: none;
   }
 
   #storage-table .table-widget-cell {
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -311,17 +311,17 @@ JSTerm.prototype = {
     let errorDocURL = response.exceptionDocURL;
 
     let errorDocLink;
     if (errorDocURL) {
       errorMessage += " ";
       errorDocLink = this.hud.document.createElementNS(XHTML_NS, "a");
       errorDocLink.className = "learn-more-link webconsole-learn-more-link";
       errorDocLink.textContent = `[${l10n.getStr("webConsoleMoreInfoLabel")}]`;
-      errorDocLink.title = errorDocURL;
+      errorDocLink.title = errorDocURL.split("?")[0];
       errorDocLink.href = "#";
       errorDocLink.draggable = false;
       errorDocLink.addEventListener("click", () => {
         this.hud.owner.openLink(errorDocURL);
       });
     }
 
     // Wrap thrown strings in Error objects, so `throw "foo"` outputs
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -20,17 +20,17 @@ ConsoleApiCall.displayName = "ConsoleApi
 ConsoleApiCall.propTypes = {
   message: PropTypes.object.isRequired,
 };
 
 function ConsoleApiCall(props) {
   const { message } = props;
   const messageBody =
     dom.span({className: "message-body devtools-monospace"},
-      formatTextContent(message.data.arguments));
+      formatTextContent(message.data));
   const icon = MessageIcon({severity: message.severity});
   const repeat = MessageRepeat({repeat: message.repeat});
   const children = [
     messageBody,
     repeat
   ];
 
   // @TODO Use of "is" is a temporary hack to get the category and severity
@@ -48,18 +48,23 @@ function ConsoleApiCall(props) {
         dom.span({className: "message-flex-body"},
           children
         )
       )
     )
   );
 }
 
-function formatTextContent(args) {
-  return args.map(function (arg, i, arr) {
+function formatTextContent(data) {
+  return data.arguments.map(function (arg, i, arr) {
+    if (data.counter) {
+      let {label, count} = data.counter;
+      arg = `${label}: ${count}`;
+    }
+
     const str = dom.span({className: "console-string"}, arg);
     if (i < arr.length - 1) {
       return [str, " "];
     }
     return str;
   });
 }
 
--- a/devtools/client/webconsole/new-console-output/test/components/test_console-api-call.html
+++ b/devtools/client/webconsole/new-console-output/test/components/test_console-api-call.html
@@ -15,19 +15,47 @@
 window.onload = Task.async(function* () {
   const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
   const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
 
   const packet = yield getPacket("console.log('foobar', 'test')", "consoleAPICall");
   const message = prepareMessage(packet);
   const rendered = renderComponent(ConsoleApiCall, {message});
 
-  const queryPath = "div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace";
-  const messageBody = rendered.querySelectorAll(queryPath);
-  const consoleStringNodes = messageBody[0].querySelectorAll("span.console-string");
+  const messageBody = getMessageBody(rendered);
+  const consoleStringNodes = getConsoleStringNodes(messageBody);
   is(consoleStringNodes.length, 2, "ConsoleApiCall outputs expected HTML structure");
-  is(messageBody[0].textContent, "foobar test", "ConsoleApiCall outputs expected text");
+  is(messageBody.textContent, "foobar test", "ConsoleApiCall outputs expected text");
+
+  for (let i = 0; i < 3; i++) {
+    const countPacket = yield getPacket("console.count('bar')", "consoleAPICall");
+    const countMessage = prepareMessage(countPacket);
+    const countRendered = renderComponent(ConsoleApiCall, {message: countMessage});
+    testConsoleCountRenderedElement(countRendered, `bar: ${i + 1}`);
+  }
 
   SimpleTest.finish()
 });
+
+function getMessageBody(renderedComponent) {
+  const queryPath = "div.message.cm-s-mozilla span span.message-flex-body " +
+    "span.message-body.devtools-monospace";
+  return renderedComponent.querySelector(queryPath);
+}
+
+function getConsoleStringNodes(messageBody) {
+  return messageBody.querySelectorAll("span.console-string");
+}
+
+function testConsoleCountRenderedElement(renderedComponent, expectedTextContent) {
+  info("Testing console.count rendered element");
+
+  const messageBody = getMessageBody(renderedComponent);
+  const consoleStringNodes = getConsoleStringNodes(messageBody);
+
+  is(consoleStringNodes.length, 1,
+    "console.count rendered element has the expected HTML structure");
+  is(messageBody.textContent, expectedTextContent,
+    "console.count rendered element has the expected text content");
+}
 </script>
 </body>
 </html>
--- a/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -10,18 +10,18 @@
 // url appended to them.
 // Bug 875456 - Log mixed content messages from the Mixed Content
 // Blocker to the Security Pane in the Web Console
 
 "use strict";
 
 const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
                  "test/test-mixedcontent-securityerrors.html";
-const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/" +
-                       "MixedContent";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+                       "Mixed_content" + DOCS_GA_PARAMS;
 
 add_task(function* () {
   yield pushPrefEnv();
 
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
--- a/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -13,18 +13,18 @@
 // appropriate messages are logged to console.
 // Bug 875456 - Log mixed content messages from the Mixed Content
 // Blocker to the Security Pane in the Web Console
 
 "use strict";
 
 const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
                  "test/test-mixedcontent-securityerrors.html";
-const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/" +
-                       "MixedContent";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+                       "Mixed_content" + DOCS_GA_PARAMS;
 
 add_task(function* () {
   yield pushPrefEnv();
 
   let { browser } = yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
--- a/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
@@ -5,18 +5,18 @@
 
 // Tests that the Web Console Mixed Content messages are displayed
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test";
 const TEST_HTTPS_URI = "https://example.com/browser/devtools/client/" +
                        "webconsole/test/test-bug-737873-mixedcontent.html";
-const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/" +
-                       "MixedContent";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+                       "Mixed_content";
 
 add_task(function* () {
   Services.prefs.setBoolPref("security.mixed_content.block_display_content",
                              false);
   Services.prefs.setBoolPref("security.mixed_content.block_active_content",
                              false);
 
   yield loadTab(TEST_URI);
--- a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
@@ -14,18 +14,18 @@ const INSECURE_PASSWORD_MSG = "Password 
                  "(http://) page. This is a security risk that allows user " +
                  "login credentials to be stolen.";
 const INSECURE_FORM_ACTION_MSG = "Password fields present in a form with an " +
                  "insecure (http://) form action. This is a security risk " +
                  "that allows user login credentials to be stolen.";
 const INSECURE_IFRAME_MSG = "Password fields present on an insecure " +
                  "(http://) iframe. This is a security risk that allows " +
                  "user login credentials to be stolen.";
-const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/docs/Security/" +
-                 "InsecurePasswords";
+const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/docs/Web/" +
+                               "Security/Insecure_passwords" + DOCS_GA_PARAMS;
 
 add_task(function* () {
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
   let result = yield waitForMessages({
     webconsole: hud,
--- a/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
+++ b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
@@ -8,17 +8,17 @@
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console HPKP invalid " +
                  "header test";
 const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
                 "test/test_hpkp-invalid-headers.sjs";
 const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
-                       "Public_Key_Pinning";
+                       "Public_Key_Pinning" + DOCS_GA_PARAMS;
 const NON_BUILTIN_ROOT_PREF = "security.cert_pinning.process_headers_from_" +
                               "non_builtin_roots";
 
 add_task(function* () {
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref(NON_BUILTIN_ROOT_PREF);
   });
 
--- a/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
+++ b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
@@ -7,18 +7,18 @@
 //  to the web console.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console HSTS invalid " +
                  "header test";
 const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
                 "test/test_hsts-invalid-headers.sjs";
-const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/" +
-                       "HTTP_Strict_Transport_Security";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+                       "HTTP_strict_transport_security" + DOCS_GA_PARAMS;
 
 add_task(function* () {
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
   yield* checkForMessage({
     url: SJS_URL + "?badSyntax",
--- a/devtools/client/webconsole/test/browser_webconsole_jsterm.js
+++ b/devtools/client/webconsole/test/browser_webconsole_jsterm.js
@@ -175,21 +175,21 @@ function* testJSTerm(hud) {
     "JSMSG_BAD_RADIX": "(42).toString(0);",
     "JSMSG_BAD_ARRAY_LENGTH": "([]).length = -1",
     "JSMSG_NEGATIVE_REPETITION_COUNT": "'abc'.repeat(-1);",
     "JSMSG_BAD_FORMAL": "var f = Function('x y', 'return x + y;');",
     "JSMSG_PRECISION_RANGE": "77.1234.toExponential(-1);",
   };
 
   for (let errorMessageName of Object.keys(ErrorDocStatements)) {
-    let url = ErrorDocs.GetURL(errorMessageName);
+    let title = ErrorDocs.GetURL({ errorMessageName }).split("?")[0];
 
     jsterm.clearOutput();
     yield jsterm.execute(ErrorDocStatements[errorMessageName]);
     yield checkResult((node) => {
-      return node.parentNode.getElementsByTagName("a")[0].title == url;
-    }, `error links to ${url}`);
+      return node.parentNode.getElementsByTagName("a")[0].title == title;
+    }, `error links to ${title}`);
   }
 
   // Ensure that dom errors, with error numbers outside of the range
   // of valid js.msg errors, don't cause crashes (bug 1270721).
   yield jsterm.execute("new Request('',{redirect:'foo'})");
 }
--- a/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
+++ b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
@@ -49,17 +49,17 @@ function* testScriptError(hud, testData)
     messages: [
       {
         category: CATEGORY_JS
       }
     ]
   });
 
   // grab the most current error doc URL
-  let url = ErrorDocs.GetURL(testData.jsmsg);
+  let url = ErrorDocs.GetURL({ errorMessageName: testData.jsmsg });
 
   let hrefs = {};
   for (let link of hud.jsterm.outputNode.querySelectorAll("a")) {
     hrefs[link.href] = true;
   }
 
   ok(url in hrefs, `Expected a link to ${url}.`);
 
--- a/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
@@ -3,19 +3,22 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Load a page with tracking elements that get blocked and make sure that a
 // 'learn more' link shows up in the webconsole.
 
 "use strict";
 
-const TEST_URI = "http://tracking.example.org/browser/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html";
-const LEARN_MORE_URI = "https://developer.mozilla.org/Firefox/Privacy/Tracking_Protection";
+const TEST_URI = "http://tracking.example.org/browser/devtools/client/" +
+                 "webconsole/test/test-trackingprotection-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/Firefox/Privacy/" +
+                       "Tracking_Protection" + DOCS_GA_PARAMS;
 const PREF = "privacy.trackingprotection.enabled";
+
 const {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
 
 registerCleanupFunction(function () {
   Services.prefs.clearUserPref(PREF);
   UrlClassifierTestUtils.cleanupTestTrackers();
 });
 
 add_task(function* testMessagesAppear() {
@@ -26,17 +29,18 @@ add_task(function* testMessagesAppear() 
 
   let hud = yield openConsole();
 
   let results = yield waitForMessages({
     webconsole: hud,
     messages: [
       {
         name: "Was blocked because tracking protection is enabled",
-        text: "The resource at \u201chttp://tracking.example.com/\u201d was blocked because tracking protection is enabled",
+        text: "The resource at \u201chttp://tracking.example.com/\u201d was " +
+              "blocked because tracking protection is enabled",
         category: CATEGORY_SECURITY,
         severity: SEVERITY_WARNING,
         objects: true,
       },
     ],
   });
 
   yield testClickOpenNewTab(hud, results[0]);
--- a/devtools/client/webconsole/test/head.js
+++ b/devtools/client/webconsole/test/head.js
@@ -36,16 +36,20 @@ const SEVERITY_LOG = 3;
 
 // The indent of a console group in pixels.
 const GROUP_INDENT = 12;
 
 const WEBCONSOLE_STRINGS_URI = "chrome://devtools/locale/" +
                                "webconsole.properties";
 var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
 
+const DOCS_GA_PARAMS = "?utm_source=mozilla" +
+                       "&utm_medium=firefox-console-errors" +
+                       "&utm_campaign=default";
+
 DevToolsUtils.testing = true;
 
 function loadTab(url) {
   let deferred = promise.defer();
 
   let tab = gBrowser.selectedTab = gBrowser.addTab(url);
   let browser = gBrowser.getBrowserForTab(tab);
 
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -39,27 +39,17 @@ loader.lazyImporter(this, "PluralForm", 
 loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts", true);
 loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
 
 const STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
 var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
-const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
-
-const TRACKING_PROTECTION_LEARN_MORE = "https://developer.mozilla.org/Firefox/Privacy/Tracking_Protection";
-
-const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
-
-const PUBLIC_KEY_PINS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Public_Key_Pinning";
-
-const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
-
-const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm";
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Mixed_content";
 
 const IGNORED_SOURCE_URLS = ["debugger eval code"];
 
 // The amount of time in milliseconds that we wait before performing a live
 // search.
 const SEARCH_DELAY = 200;
 
 // The number of lines that are displayed in the console output by default, for
@@ -1482,17 +1472,19 @@ WebConsoleFrame.prototype = {
     });
 
     let node = msg.init(this.output).render().element;
 
     // Select the body of the message node that is displayed in the console
     let msgBody = node.getElementsByClassName("message-body")[0];
 
     // Add the more info link node to messages that belong to certain categories
-    this.addMoreInfoLink(msgBody, scriptError);
+    if (scriptError.exceptionDocURL) {
+      this.addLearnMoreWarningNode(msgBody, scriptError.exceptionDocURL);
+    }
 
     // Collect telemetry data regarding JavaScript errors
     this._telemetry.logKeyed("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED",
                              scriptError.errorMessageName,
                              true);
 
     if (objectActors.size > 0) {
       node._objectActors = objectActors;
@@ -1662,75 +1654,33 @@ WebConsoleFrame.prototype = {
     linkNode.appendChild(mixedContentWarningNode);
 
     this._addMessageLinkCallback(mixedContentWarningNode, (event) => {
       event.stopPropagation();
       this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
     });
   },
 
-  /**
-   * Adds a more info link node to messages based on the nsIScriptError object
-   * that we need to report to the console
-   *
-   * @param node
-   *        The node to which we will be adding the more info link node
-   * @param scriptError
-   *        The script error object that we are reporting to the console
-   */
-  addMoreInfoLink: function (node, scriptError) {
-    let url;
-    switch (scriptError.category) {
-      case "Insecure Password Field":
-        url = INSECURE_PASSWORDS_LEARN_MORE;
-        break;
-      case "Mixed Content Message":
-      case "Mixed Content Blocker":
-        url = MIXED_CONTENT_LEARN_MORE;
-        break;
-      case "Invalid HPKP Headers":
-        url = PUBLIC_KEY_PINS_LEARN_MORE;
-        break;
-      case "Invalid HSTS Headers":
-        url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
-        break;
-      case "SHA-1 Signature":
-        url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE;
-        break;
-      case "Tracking Protection":
-        url = TRACKING_PROTECTION_LEARN_MORE;
-        break;
-      default:
-        // If all else fails check for an error doc URL.
-        url = ErrorDocs.GetURL(scriptError.errorMessageName);
-        break;
-    }
-
-    if (url) {
-      this.addLearnMoreWarningNode(node, url);
-    }
-  },
-
   /*
    * Appends a clickable warning node to the node passed
    * as a parameter to the function. When a user clicks on the appended
    * warning node, the browser navigates to the provided url.
    *
    * @param node
    *        The node to which we will be adding a clickable warning node.
    * @param url
    *        The url which points to the page where the user can learn more
    *        about security issues associated with the specific message that's
    *        being logged.
    */
   addLearnMoreWarningNode: function (node, url) {
     let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
 
     let warningNode = this.document.createElementNS(XHTML_NS, "a");
-    warningNode.title = url;
+    warningNode.title = url.split("?")[0];
     warningNode.href = url;
     warningNode.draggable = false;
     warningNode.textContent = moreInfoLabel;
     warningNode.className = "learn-more-link";
 
     this._addMessageLinkCallback(warningNode, (event) => {
       event.stopPropagation();
       this.owner.openLink(url);
--- a/devtools/server/actors/csscoverage.js
+++ b/devtools/server/actors/csscoverage.js
@@ -1,22 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { Cc, Ci, Cu } = require("chrome");
+const { Cc, Ci } = require("chrome");
 
 const Services = require("Services");
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 
 const events = require("sdk/event/core");
 const protocol = require("devtools/shared/protocol");
-const { custom } = protocol;
 const { cssUsageSpec } = require("devtools/shared/specs/csscoverage");
 
 loader.lazyGetter(this, "DOMUtils", () => {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets");
 loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true);
 
@@ -132,18 +131,17 @@ var CSSUsageActor = protocol.ActorClassW
     this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                                             .getInterface(Ci.nsIWebProgress);
     this._progress.addProgressListener(this._progressListener, this._notifyOn);
 
     if (noreload) {
       // If we're not starting by reloading the page, then pretend that onload
       // has just happened.
       this._onTabLoad(this._tabActor.window.document);
-    }
-    else {
+    } else {
       this._tabActor.window.location.reload();
     }
 
     events.emit(this, "state-change", { isRunning: true });
   },
 
   /**
    * Cease recording usage data
@@ -290,18 +288,17 @@ var CSSUsageActor = protocol.ActorClassW
       try {
         let match = document.querySelector(ruleData.test);
         if (match != null) {
           ruleData.isUsed = true;
           if (isLoad) {
             ruleData.preLoadOn.add(url);
           }
         }
-      }
-      catch (ex) {
+      } catch (ex) {
         ruleData.isError = true;
       }
     }
   },
 
   /**
    * Returns a JSONable structure designed to help marking up the style editor,
    * which describes the CSS selector usage.
@@ -338,16 +335,28 @@ var CSSUsageActor = protocol.ActorClassW
 
       reports.push(ruleReport);
     }
 
     return { reports: reports };
   },
 
   /**
+   * Compute the stylesheet URL and delegate the report creation to createEditorReport.
+   * See createEditorReport documentation.
+   *
+   * @param {StyleSheetActor} stylesheetActor
+   *        the stylesheet actor for which the coverage report should be generated.
+   */
+  createEditorReportForSheet: function (stylesheetActor) {
+    let url = sheetToUrl(stylesheetActor.rawSheet);
+    return this.createEditorReport(url);
+  },
+
+  /**
    * Returns a JSONable structure designed for the page report which shows
    * the recommended changes to a page.
    *
    * "preload" means that a rule is used before the load event happens, which
    * means that the page could by optimized by placing it in a <style> element
    * at the top of the page, moving the <link> elements to the bottom.
    *
    * Example:
@@ -411,18 +420,17 @@ var CSSUsageActor = protocol.ActorClassW
       let rules = unusedMap.get(rule.url);
       if (rules == null) {
         rules = [];
         unusedMap.set(rule.url, rules);
       }
       if (!ruleData.isUsed) {
         let ruleReport = ruleToRuleReport(rule, ruleData);
         rules.push(ruleReport);
-      }
-      else {
+      } else {
         summary.unused++;
       }
     }
     let unused = [];
     for (let [url, rules] of unusedMap) {
       unused.push({
         url: url,
         shortUrl: url.split("/").slice(-1),
@@ -440,18 +448,17 @@ var CSSUsageActor = protocol.ActorClassW
       };
 
       for (let [ruleId, ruleData] of this._knownRules) {
         if (ruleData.preLoadOn.has(url)) {
           let rule = deconstructRuleId(ruleId);
           let ruleReport = ruleToRuleReport(rule, ruleData);
           page.rules.push(ruleReport);
           summary.preload++;
-        }
-        else {
+        } else {
           summary.used++;
         }
       }
 
       if (page.rules.length > 0) {
         preload.push(page);
       }
     }
@@ -688,35 +695,32 @@ function getTestSelector(selector) {
  * I've documented all known pseudo-classes above for 2 reasons: To allow
  * checking logic and what might be missing, but also to allow a unit test
  * that fetches the list of supported pseudo-classes and pseudo-elements from
  * the platform and check that they were all represented here.
  */
 exports.SEL_ALL = [
   SEL_EXTERNAL, SEL_FORM, SEL_ELEMENT, SEL_STRUCTURAL, SEL_SEMI,
   SEL_COMBINING, SEL_MEDIA
-].reduce(function (prev, curr) { return prev.concat(curr); }, []);
+].reduce(function (prev, curr) {
+  return prev.concat(curr);
+}, []);
 
 /**
  * Find a URL for a given stylesheet
- * @param stylesheet {StyleSheet|StyleSheetActor}
+ * @param {StyleSheet} stylesheet raw stylesheet
  */
-const sheetToUrl = exports.sheetToUrl = function (stylesheet) {
+const sheetToUrl = function (stylesheet) {
   // For <link> elements
   if (stylesheet.href) {
     return stylesheet.href;
   }
 
   // For <style> elements
   if (stylesheet.ownerNode) {
     let document = stylesheet.ownerNode.ownerDocument;
     let sheets = [...document.querySelectorAll("style")];
     let index = sheets.indexOf(stylesheet.ownerNode);
     return getURL(document) + " → <style> index " + index;
   }
 
-  // When `stylesheet` is a StyleSheetActor, we don't have access to ownerNode
-  if (stylesheet.nodeHref) {
-    return stylesheet.nodeHref + " → <style> index " + stylesheet.styleSheetIndex;
-  }
-
   throw new Error("Unknown sheet source");
 };
--- a/devtools/server/actors/errordocs.js
+++ b/devtools/server/actors/errordocs.js
@@ -4,18 +4,18 @@
 
 /**
  * A mapping of error message names to external documentation. Any error message
  * included here will be displayed alongside its link in the web console.
  */
 
 "use strict";
 
-const baseURL = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/";
-
+const baseURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
+const params = "?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default";
 const ErrorDocs = {
   JSMSG_READ_ONLY: "Read-only",
   JSMSG_BAD_ARRAY_LENGTH: "Invalid_array_length",
   JSMSG_NEGATIVE_REPETITION_COUNT: "Negative_repetition_count",
   JSMSG_RESULTING_STRING_TOO_LARGE: "Resulting_string_too_large",
   JSMSG_BAD_RADIX: "Bad_radix",
   JSMSG_PRECISION_RANGE: "Precision_range",
   JSMSG_BAD_FORMAL: "Malformed_formal_parameter",
@@ -45,15 +45,39 @@ const ErrorDocs = {
   JSMSG_PAREN_AFTER_ARGS: "Missing_parenthesis_after_argument_list",
   JSMSG_MORE_ARGS_NEEDED: "More_arguments_needed",
   JSMSG_BAD_LEFTSIDE_OF_ASS: "Invalid_assignment_left-hand_side",
   JSMSG_UNTERMINATED_STRING: "Unterminated_string_literal",
   JSMSG_NOT_CONSTRUCTOR: "Not_a_constructor",
   JSMSG_CURLY_AFTER_LIST: "Missing_curly_after_property_list",
 };
 
-exports.GetURL = (errorName) => {
-  let doc = ErrorDocs[errorName];
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Mixed_content";
+const TRACKING_PROTECTION_LEARN_MORE = "https://developer.mozilla.org/Firefox/Privacy/Tracking_Protection";
+const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Insecure_passwords";
+const PUBLIC_KEY_PINS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Public_Key_Pinning";
+const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/HTTP_strict_transport_security";
+const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Weak_Signature_Algorithm";
+const ErrorCategories = {
+  "Insecure Password Field": INSECURE_PASSWORDS_LEARN_MORE,
+  "Mixed Content Message": MIXED_CONTENT_LEARN_MORE,
+  "Mixed Content Blocker": MIXED_CONTENT_LEARN_MORE,
+  "Invalid HPKP Headers": PUBLIC_KEY_PINS_LEARN_MORE,
+  "Invalid HSTS Headers": STRICT_TRANSPORT_SECURITY_LEARN_MORE,
+  "SHA-1 Signature": WEAK_SIGNATURE_ALGORITHM_LEARN_MORE,
+  "Tracking Protection": TRACKING_PROTECTION_LEARN_MORE,
+};
+
+exports.GetURL = (error) => {
+  if (!error) {
+    return;
+  }
+
+  let doc = ErrorDocs[error.errorMessageName];
   if (doc) {
-    return baseURL + doc;
+    return baseURL + doc + params;
   }
-  return undefined;
-}
+
+  let categoryURL = ErrorCategories[error.category];
+  if (categoryURL) {
+    return categoryURL + params;
+  }
+};
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -968,16 +968,19 @@ function getObjectForLocalOrSessionStora
   return {
     getNamesForHost(host) {
       let storage = this.hostVsStores.get(host);
       return Object.keys(storage);
     },
 
     getValuesForHost(host, name) {
       let storage = this.hostVsStores.get(host);
+      if (!storage) {
+        return [];
+      }
       if (name) {
         return [{name: name, value: storage.getItem(name)}];
       }
       return Object.keys(storage).map(key => {
         return {
           name: key,
           value: storage.getItem(key)
         };
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -898,17 +898,17 @@ WebConsoleActor.prototype =
                                 error.unsafeDereference();
         errorMessage = unsafeDereference && unsafeDereference.toString
           ? unsafeDereference.toString()
           : String(error);
 
           // It is possible that we won't have permission to unwrap an
           // object and retrieve its errorMessageName.
         try {
-          errorDocURL = ErrorDocs.GetURL(error && error.errorMessageName);
+          errorDocURL = ErrorDocs.GetURL(error);
         } catch (ex) {}
       }
     }
 
     // If a value is encountered that the debugger server doesn't support yet,
     // the console should remain functional.
     let resultGrip;
     try {
@@ -1444,16 +1444,17 @@ WebConsoleActor.prototype =
     let lineText = aPageError.sourceLine;
     if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) {
       lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
     }
 
     return {
       errorMessage: this._createStringGrip(aPageError.errorMessage),
       errorMessageName: aPageError.errorMessageName,
+      exceptionDocURL: ErrorDocs.GetURL(aPageError),
       sourceName: aPageError.sourceName,
       lineText: lineText,
       lineNumber: aPageError.lineNumber,
       columnNumber: aPageError.columnNumber,
       category: aPageError.category,
       timeStamp: aPageError.timeStamp,
       warning: !!(aPageError.flags & aPageError.warningFlag),
       error: !!(aPageError.flags & aPageError.errorFlag),
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -28,17 +28,17 @@ const BRAND_SHORT_NAME = Cc["@mozilla.or
 // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
 const FILENAME_DEFAULT_VALUE = " ";
 
 /*
  * There are 2 commands and 1 converter here. The 2 commands are nearly
  * identical except that one runs on the client and one in the server.
  *
  * The server command is hidden, and is designed to be called from the client
- * command when the --chrome flag is *not* used.
+ * command.
  */
 
 /**
  * Both commands have the same initial filename parameter
  */
 const filenameParam = {
   name: "filename",
   type: {
@@ -174,46 +174,24 @@ exports.items = [
     manual: l10n.lookup("screenshotManual"),
     returnType: "imageSummary",
     buttonId: "command-button-screenshot",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("screenshotTooltipPage"),
     params: [
       filenameParam,
       standardParams,
-      {
-        group: l10n.lookup("screenshotAdvancedOptions"),
-        params: [
-          {
-            name: "chrome",
-            type: "boolean",
-            description: l10n.lookupFormat("screenshotChromeDesc2", [BRAND_SHORT_NAME]),
-            manual: l10n.lookupFormat("screenshotChromeManual2", [BRAND_SHORT_NAME])
-          },
-        ]
-      },
     ],
     exec: function (args, context) {
-      if (args.chrome && args.selector) {
-        // Node screenshot with chrome option does not work as intended
-        // Refer https://bugzilla.mozilla.org/show_bug.cgi?id=659268#c7
-        // throwing for now.
-        throw new Error(l10n.lookup("screenshotSelectorChromeConflict"));
-      }
+      // Re-execute the command on the server
+      const command = context.typed.replace(/^screenshot/, "screenshot_server");
+      let capture = context.updateExec(command).then(output => {
+        return output.error ? Promise.reject(output.data) : output.data;
+      });
 
-      let capture;
-      if (!args.chrome) {
-        // Re-execute the command on the server
-        const command = context.typed.replace(/^screenshot/, "screenshot_server");
-        capture = context.updateExec(command).then(output => {
-          return output.error ? Promise.reject(output.data) : output.data;
-        });
-      } else {
-        capture = captureScreenshot(args, context.environment.chromeDocument);
-      }
       simulateCameraEffect(context.environment.chromeDocument, "shutter");
       return capture.then(saveScreenshot.bind(null, args, context));
     },
   },
   {
     item: "command",
     runAt: "server",
     name: "screenshot_server",
--- a/devtools/shared/locales/en-US/gclicommands.properties
+++ b/devtools/shared/locales/en-US/gclicommands.properties
@@ -61,36 +61,20 @@ screenshotFilenameManual=The name of the file (should have a ‘.png’ extension) to which we write the screenshot.
 # a dialog when the user is using this command.
 screenshotClipboardDesc=Copy screenshot to clipboard? (true/false)
 
 # LOCALIZATION NOTE (screenshotClipboardManual) A fuller description of the
 # 'clipboard' parameter to the 'screenshot' command, displayed when the user
 # asks for help on what it does.
 screenshotClipboardManual=True if you want to copy the screenshot instead of saving it to a file.
 
-# LOCALIZATION NOTE (screenshotChromeDesc) A very short string to describe
-# the 'chrome' parameter to the 'screenshot' command, which is displayed in
-# a dialog when the user is using this command.
-# The argument (%1$S) is the browser name.
-screenshotChromeDesc2=Capture %1$S chrome window? (true/false)
-
-# LOCALIZATION NOTE (screenshotChromeManual) A fuller description of the
-# 'chrome' parameter to the 'screenshot' command, displayed when the user
-# asks for help on what it does.
-# The argument (%1$S) is the browser name.
-screenshotChromeManual2=True if you want to take the screenshot of the %1$S window rather than the web page’s content window.
-
 # LOCALIZATION NOTE (screenshotGroupOptions) A label for the optional options of
 # the screenshot command.
 screenshotGroupOptions=Options
 
-# LOCALIZATION NOTE (screenshotGroupOptions) A label for the advanced options of
-# the screenshot command.
-screenshotAdvancedOptions=Advanced Options
-
 # LOCALIZATION NOTE (screenshotDelayDesc) A very short string to describe
 # the 'delay' parameter to the 'screenshot' command, which is displayed in
 # a dialog when the user is using this command.
 screenshotDelayDesc=Delay (seconds)
 
 # LOCALIZATION NOTE (screenshotDelayManual) A fuller description of the
 # 'delay' parameter to the 'screenshot' command, displayed when the user
 # asks for help on what it does.
@@ -111,21 +95,16 @@ screenshotDPRManual=The device pixel rat
 # a dialog when the user is using this command.
 screenshotFullPageDesc=Entire webpage? (true/false)
 
 # LOCALIZATION NOTE (screenshotFullscreenManual) A fuller description of the
 # 'fullscreen' parameter to the 'screenshot' command, displayed when the user
 # asks for help on what it does.
 screenshotFullPageManual=True if the screenshot should also include parts of the webpage which are outside the current scrolled bounds.
 
-# LOCALIZATION NOTE (screenshotSelectorChromeConflict) Exception thrown when user
-# tries to use 'selector' option along with 'chrome' option of the screenshot
-# command. Refer: https://bugzilla.mozilla.org/show_bug.cgi?id=659268#c7
-screenshotSelectorChromeConflict=selector option is not supported when chrome option is true
-
 # LOCALIZATION NOTE (screenshotGeneratedFilename) The auto generated filename
 # when no file name is provided. 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. Please don't add the extension here.
 screenshotGeneratedFilename=Screen Shot %1$S at %2$S
 
 # LOCALIZATION NOTE (screenshotErrorSavingToFile) Text displayed to user upon
 # encountering error while saving the screenshot to the file specified.
--- a/devtools/shared/specs/csscoverage.js
+++ b/devtools/shared/specs/csscoverage.js
@@ -1,15 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
 
+require("devtools/shared/specs/stylesheets");
+
 const cssUsageSpec = generateActorSpec({
   typeName: "cssUsage",
 
   events: {
     "state-change": {
       type: "stateChange",
       stateChange: Arg(0, "json")
     }
@@ -21,16 +23,20 @@ const cssUsageSpec = generateActorSpec({
     },
     stop: {},
     toggle: {},
     oneshot: {},
     createEditorReport: {
       request: { url: Arg(0, "string") },
       response: { reports: RetVal("array:json") }
     },
+    createEditorReportForSheet: {
+      request: { url: Arg(0, "stylesheet") },
+      response: { reports: RetVal("array:json") }
+    },
     createPageReport: {
       response: RetVal("json")
     },
     _testOnlyVisitedPages: {
       response: { value: RetVal("array:string") }
     },
   },
 });
--- a/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp
+++ b/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp
@@ -47,17 +47,17 @@ public:
     : ITextureClientAllocationHelper(gfx::SurfaceFormat::UNKNOWN,
                                      aSize,
                                      BackendSelector::Content,
                                      TextureFlags::DEALLOCATE_CLIENT,
                                      ALLOC_DISALLOW_BUFFERTEXTURECLIENT)
     , mGrallocFormat(aGrallocFormat)
   {}
 
-  already_AddRefed<TextureClient> Allocate(TextureForwarder* aAllocator) override
+  already_AddRefed<TextureClient> Allocate(CompositableForwarder* aAllocator) override
   {
     uint32_t usage = android::GraphicBuffer::USAGE_SW_READ_OFTEN |
                      android::GraphicBuffer::USAGE_SW_WRITE_OFTEN |
                      android::GraphicBuffer::USAGE_HW_TEXTURE;
 
     GrallocTextureData* texData = GrallocTextureData::Create(mSize, mGrallocFormat,
                                                              gfx::BackendType::NONE,
                                                              usage, aAllocator);
--- a/dom/media/platforms/omx/GonkOmxPlatformLayer.cpp
+++ b/dom/media/platforms/omx/GonkOmxPlatformLayer.cpp
@@ -11,16 +11,17 @@
 #include <media/IOMX.h>
 #include <media/stagefright/MediaCodecList.h>
 #include <utils/List.h>
 
 #include "mozilla/Monitor.h"
 #include "mozilla/layers/TextureClient.h"
 #include "mozilla/layers/GrallocTextureClient.h"
 #include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
 
 #include "ImageContainer.h"
 #include "MediaInfo.h"
 #include "OmxDataDecoder.h"
 
 
 #ifdef LOG
 #undef LOG
--- a/dom/media/test/external/mach_commands.py
+++ b/dom/media/test/external/mach_commands.py
@@ -27,24 +27,26 @@ def run_external_media_test(tests, testt
         FirefoxMediaHarness,
         MediaTestArguments,
         MediaTestRunner,
         mn_cli,
     )
 
     from mozlog.structured import commandline
 
+    from argparse import Namespace
+
     parser = MediaTestArguments()
     commandline.add_logging_group(parser)
 
     if not tests:
         tests = [os.path.join(topsrcdir,
                  'dom/media/test/external/external_media_tests/manifest.ini')]
 
-    args = parser.parse_args(args=tests)
+    args = Namespace(tests=tests)
 
     for k, v in kwargs.iteritems():
         setattr(args, k, v)
 
     parser.verify_usage(args)
 
     args.logger = commandline.setup_logging("Firefox External Media Tests",
                                             args,
--- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
@@ -61,16 +61,18 @@ public class ClientsAdapter extends Recy
         // This races when multiple Fragments are created. That's okay: one
         // will win, and thereafter, all will be okay. If we create and then
         // drop an instance the shared SharedPreferences backing all the
         // instances will maintain the state for us. Since everything happens on
         // the UI thread, this doesn't even need to be volatile.
         if (sState == null) {
             sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
         }
+
+        this.setHasStableIds(true);
     }
 
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
         final View view;
 
         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
@@ -140,16 +142,60 @@ public class ClientsAdapter extends Recy
         }
     }
 
     @Override
     public int getItemViewType(int position) {
         return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
     }
 
+    @Override
+    public long getItemId(int position) {
+        // RecyclerView.NO_ID is -1, so start our hard-coded IDs at -2.
+        final int NAVIGATION_BACK_ID = -2;
+        final int HIDDEN_DEVICES_ID = -3;
+
+        final String clientGuid;
+        // adapterList is a list of tuples (clientGuid, tabId).
+        final Pair<String, Integer> pair = adapterList.get(position);
+
+        switch (getItemTypeForPosition(position)) {
+            case NAVIGATION_BACK:
+                return NAVIGATION_BACK_ID;
+
+            case HIDDEN_DEVICES:
+                return HIDDEN_DEVICES_ID;
+
+            // For Clients, return hashCode of their GUIDs.
+            case CLIENT:
+                clientGuid = pair.first;
+                return clientGuid.hashCode();
+
+            // For Tabs, return hashCode of their URLs.
+            case CHILD:
+                clientGuid = pair.first;
+                final Integer tabId = pair.second;
+
+                final RemoteClient remoteClient = visibleClients.get(clientGuid);
+                if (remoteClient == null) {
+                    return RecyclerView.NO_ID;
+                }
+
+                final RemoteTab remoteTab = remoteClient.tabs.get(tabId);
+                if (remoteTab == null) {
+                    return RecyclerView.NO_ID;
+                }
+
+                return remoteTab.url.hashCode();
+
+            default:
+                throw new IllegalStateException("Unexpected Home Panel item type");
+        }
+    }
+
     public int getClientsCount() {
         return hiddenClients.size() + visibleClients.size();
     }
 
     @UiThread
     public void setClients(List<RemoteClient> clients) {
         adapterList.clear();
         adapterList.add(null);
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -45,16 +45,17 @@ public class CombinedHistoryAdapter exte
 
     // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
     private final SparseArray<SectionHeader> sectionHeaders;
 
     public CombinedHistoryAdapter(Resources resources) {
         super();
         sectionHeaders = new SparseArray<>();
         HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
+        this.setHasStableIds(true);
     }
 
     public void setHistory(Cursor history) {
         historyCursor = history;
         populateSectionHeaders(historyCursor, sectionHeaders);
         notifyDataSetChanged();
     }
 
@@ -185,16 +186,62 @@ public class CombinedHistoryAdapter exte
 
     @Override
     public int getItemCount() {
         final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
         return historySize + sectionHeaders.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS;
     }
 
     /**
+     * Returns stable ID for each position. Data behind historyCursor is a sorted Combined view.
+     *
+     * @param position view item position for which to generate a stable ID
+     * @return stable ID for given position
+     */
+    @Override
+    public long getItemId(int position) {
+        // Two randomly selected large primes used to generate non-clashing IDs.
+        final long PRIME_BOOKMARKS = 32416189867L;
+        final long PRIME_SECTION_HEADERS = 32416187737L;
+
+        // RecyclerView.NO_ID is -1, so let's start from -2 for our hard-coded IDs.
+        final int RECENT_TABS_ID = -2;
+        final int SYNCED_DEVICES_ID = -3;
+
+        switch (getItemTypeForPosition(position)) {
+            case RECENT_TABS:
+                return RECENT_TABS_ID;
+            case SYNCED_DEVICES:
+                return SYNCED_DEVICES_ID;
+            case SECTION_HEADER:
+                // We might have multiple section headers, so we try get unique IDs for them.
+                return position * PRIME_SECTION_HEADERS;
+            case HISTORY:
+                if (!historyCursor.moveToPosition(position)) {
+                    return RecyclerView.NO_ID;
+                }
+
+                final int historyIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID);
+                final long historyId = historyCursor.getLong(historyIdCol);
+
+                if (historyId != -1) {
+                    return historyId;
+                }
+
+                final int bookmarkIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
+                final long bookmarkId = historyCursor.getLong(bookmarkIdCol);
+
+                // Avoid clashing with historyId.
+                return bookmarkId * PRIME_BOOKMARKS;
+            default:
+                throw new IllegalStateException("Unexpected Home Panel item type");
+        }
+    }
+
+    /**
      * Add only the SectionHeaders that have history items within their range to a SparseArray, where the
      * array index is the position of the header in the history-only (no clients) ordering.
      * @param c data Cursor
      * @param sparseArray SparseArray to populate
      */
     private static void populateSectionHeaders(Cursor c, SparseArray<SectionHeader> sparseArray) {
         sparseArray.clear();
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
@@ -521,16 +521,29 @@ public class TopSitesPanel extends HomeF
             return Math.max(0, super.getCount() - mMaxGridEntries);
         }
 
         @Override
         public Object getItem(int position) {
             return super.getItem(position + mMaxGridEntries);
         }
 
+        /**
+         * We have to override default getItemId implementation, since for a given position, it returns
+         * value of the _id column. In our case _id is always 0 (see Combined view).
+         */
+        @Override
+        public long getItemId(int position) {
+            final int adjustedPosition = position + mMaxGridEntries;
+            final Cursor cursor = getCursor();
+
+            cursor.moveToPosition(adjustedPosition);
+            return getItemIdForTopSitesCursor(cursor);
+        }
+
         @Override
         public void bindView(View view, Context context, Cursor cursor) {
             final int position = cursor.getPosition();
             cursor.moveToPosition(position + mMaxGridEntries);
 
             final TwoLinePageRow row = (TwoLinePageRow) view;
             row.updateFromCursor(cursor);
         }
@@ -580,33 +593,26 @@ public class TopSitesPanel extends HomeF
                 // This will force each view to load favicons for the missing
                 // thumbnails if necessary.
                 gridItem.markAsDirty();
             }
 
             notifyDataSetChanged();
         }
 
+        /**
+         * We have to override default getItemId implementation, since for a given position, it returns
+         * value of the _id column. In our case _id is always 0 (see Combined view).
+         */
         @Override
         public long getItemId(int position) {
-            // We are trying to return stable ids so that Android can recycle views appropriately:
-            // * If we have a history id then we return it
-            // * If we only have a bookmark id then we negate it and return it. We negate it in order
-            //   to avoid clashing/conflicting with history ids.
-
             final Cursor cursor = getCursor();
             cursor.moveToPosition(position);
 
-            final long historyId = cursor.getLong(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
-            if (historyId != 0) {
-                return historyId;
-            }
-
-            final long bookmarkId = cursor.getLong(cursor.getColumnIndexOrThrow(TopSites.BOOKMARK_ID));
-            return -1 * bookmarkId;
+            return getItemIdForTopSitesCursor(cursor);
         }
 
         @Override
         public void bindView(View bindView, Context context, Cursor cursor) {
             final String url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
             final String title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
             final int type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
 
@@ -960,9 +966,30 @@ public class TopSitesPanel extends HomeF
 
         @Override
         public void onLoaderReset(Loader<Map<String, ThumbnailInfo>> loader) {
             if (mGridAdapter != null) {
                 mGridAdapter.updateThumbnails(null);
             }
         }
     }
+
+    /**
+     * We are trying to return stable IDs so that Android can recycle views appropriately:
+     * - If we have a history ID then we return it
+     * - If we only have a bookmark ID then we negate it and return it. We negate it in order
+     *   to avoid clashing/conflicting with history IDs.
+     *
+     * @param cursorInPosition Cursor already moved to position for which we're getting a stable ID
+     * @return Stable ID for a given cursor
+     */
+    private static long getItemIdForTopSitesCursor(final Cursor cursorInPosition) {
+        final int historyIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.HISTORY_ID);
+        final long historyId = cursorInPosition.getLong(historyIdCol);
+        if (historyId != 0) {
+            return historyId;
+        }
+
+        final int bookmarkIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
+        final long bookmarkId = cursorInPosition.getLong(bookmarkIdCol);
+        return -1 * bookmarkId;
+    }
 }
--- a/services/common/rest.js
+++ b/services/common/rest.js
@@ -600,16 +600,20 @@ RESTRequest.prototype = {
                     newChannel.URI.spec + ", internal = " + isInternal);
     return isInternal && isSameURI;
   },
 
   /*** nsIChannelEventSink ***/
   asyncOnChannelRedirect:
     function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
 
+    let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>";
+    let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>";
+    this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags);
+
     try {
       newChannel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       this._log.error("Unexpected error: channel not nsIHttpChannel!");
       callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
       return;
     }
 
--- a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
+++ b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
@@ -87,10 +87,31 @@ var Authentication = {
         Logger.AssertEqual(Weave.Status.login, Weave.LOGIN_SUCCEEDED,
                            "Weave logged in");
       }
 
       return true;
     } catch (error) {
       throw new Error("signIn() failed with: " + error.message);
     }
+  },
+
+  /**
+   * Sign out of Firefox Accounts. It also clears out the device ID, if we find one.
+   */
+  signOut() {
+    if (Authentication.isLoggedIn) {
+      let user = Authentication.getSignedInUser();
+      if (!user) {
+        throw new Error("Failed to get signed in user!");
+      }
+      let fxc = new FxAccountsClient();
+      let { sessionToken, deviceId } = user;
+      if (deviceId) {
+        Logger.logInfo("Destroying device " + deviceId);
+        Async.promiseSpinningly(fxc.signOutAndDestroyDevice(sessionToken, deviceId, { service: "sync" }));
+      } else {
+        Logger.logError("No device found.");
+        Async.promiseSpinningly(fxc.signOut(sessionToken, { service: "sync" }));
+      }
+    }
   }
 };
--- a/services/sync/tps/extensions/tps/resource/auth/sync.jsm
+++ b/services/sync/tps/extensions/tps/resource/auth/sync.jsm
@@ -75,10 +75,14 @@ var Authentication = {
                          "Weave logged in");
 
       // Bug 997279: Temporary workaround until we can ensure that Sync itself
       // sends this notification for the first login attempt by TPS
       Weave.Svc.Obs.notify("weave:service:setup-complete");
     }
 
     return true;
+  },
+
+  signOut() {
+    Weave.Service.logout();
   }
 };
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -92,17 +92,16 @@ const OBSERVER_TOPICS = ["fxaccounts:onl
                          "weave:service:sync:start"
                         ];
 
 var TPS = {
   _currentAction: -1,
   _currentPhase: -1,
   _enabledEngines: null,
   _errors: 0,
-  _finalPhase: false,
   _isTracking: false,
   _operations_pending: 0,
   _phaseFinished: false,
   _phaselist: {},
   _setupComplete: false,
   _syncActive: false,
   _syncErrors: 0,
   _syncWipeAction: null,
@@ -159,23 +158,16 @@ var TPS = {
       Logger.logInfo("----------event observed: " + topic);
 
       switch(topic) {
         case "private-browsing":
           Logger.logInfo("private browsing " + data);
           break;
 
         case "quit-application-requested":
-          // Ensure that we eventually wipe the data on the server
-          if (this._errors || !this._phaseFinished || this._finalPhase) {
-            try {
-              this.WipeServer();
-            } catch (ex) {}
-          }
-
           OBSERVER_TOPICS.forEach(function(topic) {
             Services.obs.removeObserver(this, topic);
           }, this);
 
           Logger.close();
 
           break;
 
@@ -581,17 +573,28 @@ var TPS = {
       }, 2000, this, "postmozmilltest");
     }
   },
 
   MozmillSetTestListener: function TPS__MozmillSetTestListener(obj) {
     Logger.logInfo("mozmill setTest: " + obj.name);
   },
 
-
+  Cleanup() {
+    try {
+      this.WipeServer();
+    } catch (ex) {
+      Logger.logError("Failed to wipe server: " + Log.exceptionStr(ex));
+    }
+    try {
+      Authentication.signOut();
+    } catch (e) {
+      Logger.logError("Failed to sign out: " + Log.exceptionStr(e));
+    }
+  },
 
   /**
    * Use Sync's bookmark validation code to see if we've corrupted the tree.
    */
   ValidateBookmarks() {
 
     let getServerBookmarkState = () => {
       let bookmarkEngine = Weave.Service.engineManager.get('bookmarks');
@@ -651,17 +654,17 @@ var TPS = {
       this.DumpError("Bookmark validation failed", e);
     }
     Logger.logInfo("Bookmark validation finished");
   },
 
   RunNextTestAction: function() {
     try {
       if (this._currentAction >=
-          this._phaselist["phase" + this._currentPhase].length) {
+          this._phaselist[this._currentPhase].length) {
         if (this.shouldValidateBookmarks) {
           // Run bookmark validation and then finish up
           this.ValidateBookmarks();
         }
         // we're all done
         Logger.logInfo("test phase " + this._currentPhase + ": " +
                        (this._errors ? "FAIL" : "PASS"));
         this._phaseFinished = true;
@@ -671,17 +674,17 @@ var TPS = {
 
       if (this.seconds_since_epoch)
         this._usSinceEpoch = this.seconds_since_epoch * 1000 * 1000;
       else {
         this.DumpError("seconds-since-epoch not set");
         return;
       }
 
-      let phase = this._phaselist["phase" + this._currentPhase];
+      let phase = this._phaselist[this._currentPhase];
       let action = phase[this._currentAction];
       Logger.logInfo("starting action: " + action[0].name);
       action[0].apply(this, action.slice(1));
 
       // if we're in an async operation, don't continue on to the next action
       if (this._operations_pending)
         return;
 
@@ -768,24 +771,31 @@ var TPS = {
    *
    * This is called by RunTestPhase() after the environment is validated.
    */
   _executeTestPhase: function _executeTestPhase(file, phase, settings) {
     try {
       // parse the test file
       Services.scriptloader.loadSubScript(file, this);
       this._currentPhase = phase;
-      let this_phase = this._phaselist["phase" + this._currentPhase];
+      if (this._currentPhase.startsWith("cleanup-")) {
+        let profileToClean = Cc["@mozilla.org/toolkit/profile-service;1"]
+                             .getService(Ci.nsIToolkitProfileService)
+                             .selectedProfile.name;
+        this.phases[this._currentPhase] = profileToClean;
+        this.Phase(this._currentPhase, [[this.Cleanup]]);
+      }
+      let this_phase = this._phaselist[this._currentPhase];
 
       if (this_phase == undefined) {
         this.DumpError("invalid phase " + this._currentPhase);
         return;
       }
 
-      if (this.phases["phase" + this._currentPhase] == undefined) {
+      if (this.phases[this._currentPhase] == undefined) {
         this.DumpError("no profile defined for phase " + this._currentPhase);
         return;
       }
 
       // If we have restricted the active engines, unregister engines we don't
       // care about.
       if (settings.ignoreUnusedEngines && Array.isArray(this._enabledEngines)) {
         let names = {};
@@ -795,36 +805,20 @@ var TPS = {
 
         for (let engine of Weave.Service.engineManager.getEnabled()) {
           if (!(engine.name in names)) {
             Logger.logInfo("Unregistering unused engine: " + engine.name);
             Weave.Service.engineManager.unregister(engine);
           }
         }
       }
-
-      Logger.logInfo("Starting phase " + parseInt(phase, 10) + "/" +
-                     Object.keys(this._phaselist).length);
-
-      Logger.logInfo("setting client.name to " + this.phases["phase" + this._currentPhase]);
-      Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
+      Logger.logInfo("Starting phase " + this._currentPhase);
 
-      // TODO Phases should be defined in a data type that has strong
-      // ordering, not by lexical sorting.
-      let currentPhase = parseInt(this._currentPhase, 10);
-
-      // Login at the beginning of the test.
-      if (currentPhase <= 1) {
-        this_phase.unshift([this.Login]);
-      }
-
-      // Wipe the server at the end of the final test phase.
-      if (currentPhase >= Object.keys(this.phases).length) {
-        this._finalPhase = true;
-      }
+      Logger.logInfo("setting client.name to " + this.phases[this._currentPhase]);
+      Weave.Svc.Prefs.set("client.name", this.phases[this._currentPhase]);
 
       // If a custom server was specified, set it now
       if (this.config["serverURL"]) {
         Weave.Service.serverURL = this.config.serverURL;
         prefs.setCharPref('tps.serverURL', this.config.serverURL);
       }
 
       // Store account details as prefs so they're accessible to the Mozmill
@@ -854,16 +848,20 @@ var TPS = {
    * This is called when loading individual test files.
    *
    * @param  phasename
    *         String name of the phase being loaded.
    * @param  fnlist
    *         Array of functions/actions to perform.
    */
   Phase: function Test__Phase(phasename, fnlist) {
+    if (Object.keys(this._phaselist).length === 0) {
+      // This is the first phase, add that we need to login.
+      fnlist.unshift([this.Login]);
+    }
     this._phaselist[phasename] = fnlist;
   },
 
   /**
    * Restrict enabled Sync engines to a specified set.
    *
    * This can be called by a test to limit what engines are enabled. It is
    * recommended to call it to reduce the overhead and log clutter for the
--- a/testing/firefox-ui/mach_commands.py
+++ b/testing/firefox-ui/mach_commands.py
@@ -25,16 +25,17 @@ def setup_argument_parser_functional():
 
 def setup_argument_parser_update():
     from firefox_ui_harness.arguments.update import UpdateArguments
     return UpdateArguments()
 
 
 def run_firefox_ui_test(testtype=None, topsrcdir=None, **kwargs):
     from mozlog.structured import commandline
+    from argparse import Namespace
     import firefox_ui_harness
 
     if testtype == 'functional':
         parser = setup_argument_parser_functional()
     else:
         parser = setup_argument_parser_update()
 
     test_types = {
@@ -62,18 +63,17 @@ def run_firefox_ui_test(testtype=None, t
     # If no tests have been selected, set default ones
     if not kwargs.get('tests'):
         kwargs['tests'] = [os.path.join(fxui_dir, 'tests', test)
                            for test in test_types[testtype]['default_tests']]
 
     kwargs['logger'] = commandline.setup_logging('Firefox UI - {} Tests'.format(testtype),
                                                  {"mach": sys.stdout})
 
-    # pass tests to parse_args to avoid rereading sys.argv
-    args = parser.parse_args(args=kwargs['tests'])
+    args = Namespace()
 
     for k, v in kwargs.iteritems():
         setattr(args, k, v)
 
     parser.verify_usage(args)
 
     failed = test_types[testtype]['cli_module'].cli(args=vars(args))
 
--- a/testing/marionette/harness/marionette/runner/base.py
+++ b/testing/marionette/harness/marionette/runner/base.py
@@ -370,70 +370,71 @@ class BaseMarionetteArguments(ArgumentPa
     def register_argument_container(self, container):
         group = self.add_argument_group(container.name)
 
         for cli, kwargs in container.args:
             group.add_argument(*cli, **kwargs)
 
         self.argument_containers.append(container)
 
-    def parse_args(self, args=None, values=None):
-        args = ArgumentParser.parse_args(self, args, values)
+    def parse_known_args(self, args=None, namespace=None):
+        args, remainder = ArgumentParser.parse_known_args(self, args, namespace)
         for container in self.argument_containers:
             if hasattr(container, 'parse_args_handler'):
                 container.parse_args_handler(args)
-        return args
+        return (args, remainder)
 
     def _get_preferences(self, prefs_files, prefs_args):
         """
         return user defined profile preferences as a dict
         """
         # object that will hold the preferences
         prefs = mozprofile.prefs.Preferences()
 
         # add preferences files
         if prefs_files:
             for prefs_file in prefs_files:
                 prefs.add_file(prefs_file)
 
         separator = ':'
         cli_prefs = []
         if prefs_args:
+            misformatted = []
             for pref in prefs_args:
                 if separator not in pref:
-                    continue
-                cli_prefs.append(pref.split(separator, 1))
-
+                    misformatted.append(pref)
+                else:
+                    cli_prefs.append(pref.split(separator, 1))
+            if misformatted:
+                self._print_message("Warning: Ignoring preferences not in key{}value format: {}\n"
+                                    .format(separator, ", ".join(misformatted)))
         # string preferences
         prefs.add(cli_prefs, cast=True)
 
         return dict(prefs())
 
     def verify_usage(self, args):
         if not args.tests:
-            print 'must specify one or more test files, manifests, or directories'
-            sys.exit(1)
+            self.error('You must specify one or more test files, manifests, or directories.')
 
-        for path in args.tests:
-            if not os.path.exists(path):
-                print '{0} does not exist'.format(path)
-                sys.exit(1)
+        missing_tests = [path for path in args.tests if not os.path.exists(path)]
+        if missing_tests:
+            self.error("Test file(s) not found: " + " ".join([path for path in missing_tests]))
 
         if not args.address and not args.binary:
-            print 'must specify --binary, or --address'
-            sys.exit(1)
+            self.error('You must specify --binary, or --address')
 
         if args.total_chunks is not None and args.this_chunk is None:
             self.error('You must specify which chunk to run.')
 
         if args.this_chunk is not None and args.total_chunks is None:
             self.error('You must specify how many chunks to split the tests into.')
 
         if args.total_chunks is not None:
-            if not 1 <= args.total_chunks:
+            if not 1 < args.total_chunks:
                 self.error('Total chunks must be greater than 1.')
             if not 1 <= args.this_chunk <= args.total_chunks:
                 self.error('Chunk to run must be between 1 and %s.' % args.total_chunks)
 
         if args.jsdebugger:
             args.app_args.append('-jsdebugger')
             args.socket_timeout = None
 
--- a/testing/marionette/mach_commands.py
+++ b/testing/marionette/mach_commands.py
@@ -35,18 +35,19 @@ def run_marionette(tests, testtype=None,
 
     parser = BaseMarionetteArguments()
     commandline.add_logging_group(parser)
 
     if not tests:
         tests = [os.path.join(topsrcdir,
                  'testing/marionette/harness/marionette/tests/unit-tests.ini')]
 
-    args = parser.parse_args(args=tests)
+    args = argparse.Namespace(tests=tests)
 
+    args.address = address
     args.binary = binary
 
     for k, v in kwargs.iteritems():
         setattr(args, k, v)
 
     parser.verify_usage(args)
 
     args.logger = commandline.setup_logging("Marionette Unit Tests",
@@ -78,17 +79,17 @@ def run_session(tests, testtype=None, ad
 
     parser = BaseSessionArguments()
     commandline.add_logging_group(parser)
 
     if not tests:
         tests = [os.path.join(topsrcdir,
                  'testing/marionette/harness/session/tests/unit-tests.ini')]
 
-    args = parser.parse_args(args=tests)
+    args = argparse.Namespace(tests=tests)
 
     args.binary = binary
 
     for k, v in kwargs.iteritems():
         setattr(args, k, v)
 
     parser.verify_usage(args)
 
--- a/testing/tps/tps/phase.py
+++ b/testing/tps/tps/phase.py
@@ -2,46 +2,40 @@
 # 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/.
 
 import re
 
 class TPSTestPhase(object):
 
     lineRe = re.compile(
-        r'^(.*?)test phase (?P<matchphase>\d+): (?P<matchstatus>.*)$')
+        r'^(.*?)test phase (?P<matchphase>[^\s]+): (?P<matchstatus>.*)$')
 
     def __init__(self, phase, profile, testname, testpath, logfile, env,
                  firefoxRunner, logfn, ignore_unused_engines=False):
         self.phase = phase
         self.profile = profile
         self.testname = str(testname) # this might be passed in as unicode
         self.testpath = testpath
         self.logfile = logfile
         self.env = env
         self.firefoxRunner = firefoxRunner
         self.log = logfn
         self.ignore_unused_engines = ignore_unused_engines
         self._status = None
         self.errline = ''
 
     @property
-    def phasenum(self):
-        match = re.match('.*?(\d+)', self.phase)
-        if match:
-            return match.group(1)
-
-    @property
     def status(self):
         return self._status if self._status else 'unknown'
 
     def run(self):
         # launch Firefox
         args = [ '-tps', self.testpath,
-                 '-tpsphase', self.phasenum,
+                 '-tpsphase', self.phase,
                  '-tpslogfile', self.logfile ]
 
         if self.ignore_unused_engines:
             args.append('--ignore-unused-engines')
 
         self.log('\nLaunching Firefox for phase %s with args %s\n' %
                  (self.phase, str(args)))
         self.firefoxRunner.run(env=self.env,
@@ -58,17 +52,17 @@ class TPSTestPhase(object):
                 if line.find('Running test %s' % self.testname) > -1:
                     found_test = True
                 else:
                     continue
 
             # look for the status of the current phase
             match = self.lineRe.match(line)
             if match:
-                if match.group('matchphase') == self.phasenum:
+                if match.group('matchphase') == self.phase:
                     self._status = match.group('matchstatus')
                     break
 
             # set the status to FAIL if there is TPS error
             if line.find('CROSSWEAVE ERROR: ') > -1 and not self._status:
                 self._status = 'FAIL'
                 self.errline = line[line.find('CROSSWEAVE ERROR: ') + len('CROSSWEAVE ERROR: '):]
 
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -198,16 +198,35 @@ class TPSTestRunner(object):
             zip.write(os.path.join(rootDir, dir), dir)
         except:
             # on some OS's, adding directory entries doesn't seem to work
             pass
         for root, dirs, files in os.walk(os.path.join(rootDir, dir)):
             for f in files:
                 zip.write(os.path.join(root, f), os.path.join(dir, f))
 
+    def handle_phase_failure(self, profiles):
+        for profile in profiles:
+            self.log('\nDumping sync log for profile %s\n' %  profiles[profile].profile)
+            for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
+                for f in files:
+                    weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
+                    if os.access(weavelog, os.F_OK):
+                        with open(weavelog, 'r') as fh:
+                            for line in fh:
+                                possible_time = line[0:13]
+                                if len(possible_time) == 13 and possible_time.isdigit():
+                                    time_ms = int(possible_time)
+                                    formatted = time.strftime('%Y-%m-%d %H:%M:%S',
+                                            time.localtime(time_ms / 1000))
+                                    self.log('%s.%03d %s' % (
+                                        formatted, time_ms % 1000, line[14:] ))
+                                else:
+                                    self.log(line)
+
     def run_single_test(self, testdir, testname):
         testpath = os.path.join(testdir, testname)
         self.log("Running test %s\n" % testname, True)
 
         # Read and parse the test file, merge it with the contents of the config
         # file, and write the combined output to a temporary file.
         f = open(testpath, 'r')
         testcontent = f.read()
@@ -246,39 +265,40 @@ class TPSTestRunner(object):
                 self.firefoxRunner,
                 self.log,
                 ignore_unused_engines=self.ignore_unused_engines))
 
         # sort the phase list by name
         phaselist = sorted(phaselist, key=lambda phase: phase.phase)
 
         # run each phase in sequence, aborting at the first failure
+        failed = False
         for phase in phaselist:
             phase.run()
+            if phase.status != 'PASS':
+                failed = True
+                break;
 
-            # if a failure occurred, dump the entire sync log into the test log
-            if phase.status != 'PASS':
-                for profile in profiles:
-                    self.log('\nDumping sync log for profile %s\n' %  profiles[profile].profile)
-                    for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
-                        for f in files:
-                            weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
-                            if os.access(weavelog, os.F_OK):
-                                with open(weavelog, 'r') as fh:
-                                    for line in fh:
-                                        possible_time = line[0:13]
-                                        if len(possible_time) == 13 and possible_time.isdigit():
-                                            time_ms = int(possible_time)
-                                            formatted = time.strftime('%Y-%m-%d %H:%M:%S',
-                                                    time.localtime(time_ms / 1000))
-                                            self.log('%s.%03d %s' % (
-                                                formatted, time_ms % 1000, line[14:] ))
-                                        else:
-                                            self.log(line)
-                break;
+        for profilename in profiles:
+            cleanup_phase = TPSTestPhase(
+                'cleanup-' + profilename,
+                profiles[profilename], testname,
+                tmpfile.filename,
+                self.logfile,
+                self.env,
+                self.firefoxRunner,
+                self.log)
+
+            cleanup_phase.run()
+            if cleanup_phase.status != 'PASS':
+                failed = True
+                # Keep going to run the remaining cleanup phases.
+
+        if failed:
+            self.handle_phase_failure(profiles)
 
         # grep the log for FF and sync versions
         f = open(self.logfile)
         logdata = f.read()
         match = self.syncVerRe.search(logdata)
         sync_version = match.group('syncversion') if match else 'unknown'
         match = self.ffVerRe.search(logdata)
         firefox_version = match.group('ffver') if match else 'unknown'
@@ -326,18 +346,16 @@ class TPSTestRunner(object):
                         'message': result[1],
                         'state': result[0],
                         'logdata': logdata
                       })
 
         self.log(logstr, True)
         for phase in phaselist:
             print "\t%s: %s" % (phase.phase, phase.status)
-            if phase.status == 'FAIL':
-                break
 
         return resultdata
 
     def update_preferences(self):
         self.preferences = self.default_preferences.copy()
 
         if self.mobile:
             self.preferences.update({'services.sync.client.type' : 'mobile'})