Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 01 Jul 2016 11:18:56 +0200
changeset 383209 0a0baf81a9a7269455f77bb22b70207f9597abb7
parent 383208 56715a33b016d05afc2541e19fd7e0c907258971 (current diff)
parent 383145 fdcee57b4e4f66a82831ab01e61500da98a858e8 (diff)
child 383210 95006e936e445294d749d1e1979648bb4ef0e8a1
push id21963
push userdmitchell@mozilla.com
push dateFri, 01 Jul 2016 19:54:18 +0000
milestone50.0a1
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'})