Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 25 Aug 2014 16:54:06 -0700
changeset 223080 0736ab6957b2de9c87c05e41be8c356dcb1bf3b1
parent 223071 f22cf1debda460ec1cb404b327d854d12ac0d3fc (current diff)
parent 223079 bef8c1d8b3bd76c3cdee88d89e4048c26f3989ec (diff)
child 223129 dc352a7bf234df7682ceedbf70b1f676e82e8171
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -1,13 +1,16 @@
 # -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 # 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/.
 
+# NB: IF YOU ADD ITEMS TO THIS FILE, PLEASE UPDATE THE WHITELIST IN
+# BrowserUITelemetry.jsm. SEE BUG 991757 FOR DETAILS.
+
       <menugroup id="context-navigation">
         <menuitem id="context-back"
                   class="menuitem-iconic"
                   tooltiptext="&backButton.tooltip;"
                   aria-label="&backCmd.label;"
                   command="Browser:BackOrBackDuplicate"
                   onclick="checkForMiddleClick(this, event);"/>
         <menuitem id="context-forward"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -33,23 +33,31 @@ nsContextMenu.prototype = {
                                                    Ci.nsIPrefLocalizedString).data;
     } catch (e) { }
 
     this.isContentSelected = this.isContentSelection();
     this.onPlainTextLink = false;
 
     // Initialize (disable/remove) menu items.
     this.initItems();
+
+    // Register this opening of the menu with telemetry:
+    this._checkTelemetryForMenu(aXulMenu);
   },
 
   hiding: function CM_hiding() {
     gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
+
+    // This handler self-deletes, only run it if it is still there:
+    if (this._onPopupHiding) {
+      this._onPopupHiding();
+    }
   },
 
   initItems: function CM_initItems() {
     this.initPageMenuSeparator();
     this.initOpenItems();
     this.initNavigationItems();
     this.initViewItems();
     this.initMiscItems();
@@ -1698,10 +1706,81 @@ nsContextMenu.prototype = {
       engineName = ss.defaultEngine.name;
 
     // format "Search <engine> for <selection>" string to show in menu
     var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
                                                         [engineName,
                                                          selectedText]);
     menuItem.label = menuLabel;
     menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
-  }
+  },
+
+  _getTelemetryClickInfo: function(aXulMenu) {
+    this._onPopupHiding = () => {
+      aXulMenu.ownerDocument.removeEventListener("command", activationHandler, true);
+      aXulMenu.removeEventListener("popuphiding", this._onPopupHiding, true);
+      delete this._onPopupHiding;
+
+      let eventKey = [
+          this._telemetryPageContext,
+          this._telemetryHadCustomItems ? "withcustom" : "withoutcustom"
+      ];
+      let target = this._telemetryClickID || "close-without-interaction";
+      BrowserUITelemetry.registerContextMenuInteraction(eventKey, target);
+    };
+    let activationHandler = (e) => {
+      // Deal with command events being routed to command elements; figure out
+      // what triggered the event (which will have the right e.target)
+      if (e.sourceEvent) {
+        e = e.sourceEvent;
+      }
+      // Target should be in the menu (this catches using shortcuts for items
+      // not in the menu while the menu is up)
+      if (!aXulMenu.contains(e.target)) {
+        return;
+      }
+
+      // Check if this is a page menu item:
+      if (e.target.hasAttribute(PageMenu.GENERATEDITEMID_ATTR)) {
+        this._telemetryClickID = "custom-page-item";
+      } else {
+        this._telemetryClickID = (e.target.id || "unknown").replace(/^context-/i, "");
+      }
+    };
+    aXulMenu.ownerDocument.addEventListener("command", activationHandler, true);
+    aXulMenu.addEventListener("popuphiding", this._onPopupHiding, true);
+  },
+
+  _getTelemetryPageContextInfo: function() {
+    if (this.isContentSelected) {
+      return "selection";
+    }
+    if (this.onLink) {
+      if (this.onImage || this.onCanvas) {
+        return "image-link";
+      }
+      return "link";
+    }
+    if (this.onImage) {
+      return "image"
+    }
+    if (this.onCanvas) {
+      return "canvas";
+    }
+    if (this.onVideo || this.onAudio) {
+      return "media";
+    }
+    if (this.onTextInput) {
+      return "input";
+    }
+    if (this.onSocial) {
+      return "social";
+    }
+    return "other";
+  },
+
+  _checkTelemetryForMenu: function(aXulMenu) {
+    this._telemetryClickID = null;
+    this._telemetryPageContext = this._getTelemetryPageContextInfo();
+    this._telemetryHadCustomItems = this.hasPageMenu;
+    this._getTelemetryClickInfo(aXulMenu);
+  },
 };
--- a/browser/devtools/inspector/test/browser_inspector_initialization.js
+++ b/browser/devtools/inspector/test/browser_inspector_initialization.js
@@ -114,11 +114,14 @@ function* clickOnInspectMenuItem(node) {
   info("Clicking on 'Inspect Element' context menu item of " + node);
   document.popupNode = node;
   var contentAreaContextMenu = getNode("#contentAreaContextMenu", { document });
   var contextMenu = new nsContextMenu(contentAreaContextMenu);
 
   info("Triggering inspect action.");
   yield contextMenu.inspectNode();
 
+  // Clean up context menu:
+  contextMenu.hiding();
+
   info("Waiting for inspector to update.");
   yield getActiveInspector().once("inspector-updated");
 }
--- a/browser/devtools/styleinspector/test/browser_ruleview_content_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_content_02.js
@@ -26,16 +26,20 @@ let test = asyncTest(function*() {
 
   info("Opening the inspector using the content context-menu");
   let onInspectorReady = gDevTools.once("inspector-ready");
 
   document.popupNode = element;
   let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
   let contextMenu = new nsContextMenu(contentAreaContextMenu);
   yield contextMenu.inspectNode();
+
+  // Clean up context menu:
+  contextMenu.hiding();
+
   yield onInspectorReady;
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = gDevTools.getToolbox(target);
 
   info("Getting the inspector and making sure it is fully updated");
   let inspector = toolbox.getPanel("inspector");
   yield inspector.once("inspector-updated");
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -556,21 +556,17 @@ let UI = {
         debugCmd.removeAttribute("disabled");
       } else {
         stopCmd.setAttribute("disabled", "true");
         debugCmd.setAttribute("disabled", "true");
       }
 
       // If connected and a project is selected
       if (AppManager.selectedProject.type == "runtimeApp") {
-        if (isProjectRunning) {
-          playCmd.setAttribute("disabled", "true");
-        } else {
-          playCmd.removeAttribute("disabled");
-        }
+        playCmd.removeAttribute("disabled");
       } else if (AppManager.selectedProject.type == "mainProcess") {
         playCmd.setAttribute("disabled", "true");
         stopCmd.setAttribute("disabled", "true");
       } else {
         if (AppManager.selectedProject.errorsCount == 0) {
           playCmd.removeAttribute("disabled");
         } else {
           playCmd.setAttribute("disabled", "true");
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -378,17 +378,21 @@ exports.AppManager = AppManager = {
 
   runRuntimeApp: function() {
     if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
       return promise.reject("attempting to run a non-runtime app");
     }
     let client = this.connection.client;
     let actor = this._listTabsResponse.webappsActor;
     let manifest = this.getProjectManifestURL(this.selectedProject);
-    return AppActorFront.launchApp(client, actor, manifest);
+    if (!this.isProjectRunning()) {
+      return AppActorFront.launchApp(client, actor, manifest);
+    } else {
+      return AppActorFront.reloadApp(client, actor, manifest);
+    }
   },
 
   installAndRunProject: function() {
     let project = this.selectedProject;
 
     if (!project || (project.type != "packaged" && project.type != "hosted")) {
       console.error("Can't install project. Unknown type of project.");
       return promise.reject("Can't install");
--- a/browser/devtools/webide/themes/webide.css
+++ b/browser/devtools/webide/themes/webide.css
@@ -130,17 +130,17 @@ window.busy-determined #action-busy-unde
 /* Panels */
 
 panel > vbox {
   overflow-x: hidden;
 }
 
 panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 12px 0;
-  width: 200px;
+  min-width: 200px;
 }
 
 .panel-item {
   padding: 3px 12px;
   margin: 0;
   -moz-appearance: none;
   border-width: 0;
   font-size: 13px; /* height of the icons */
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -165,16 +165,18 @@ const BUCKET_PREFIX = "bucket_";
 // Standard separator to use between different parts of a bucket name, such
 // as primary name and the time step string.
 const BUCKET_SEPARATOR = "|";
 
 this.BrowserUITelemetry = {
   init: function() {
     UITelemetry.addSimpleMeasureFunction("toolbars",
                                          this.getToolbarMeasures.bind(this));
+    UITelemetry.addSimpleMeasureFunction("contextmenu",
+                                         this.getContextMenuInfo.bind(this));
     // Ensure that UITour.jsm remains lazy-loaded, yet always registers its
     // simple measure function with UITelemetry.
     UITelemetry.addSimpleMeasureFunction("UITour",
                                          () => UITour.getTelemetry());
 
     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
     Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
     CustomizableUI.addListener(this);
@@ -215,21 +217,23 @@ this.BrowserUITelemetry = {
    * example["c"]++;
    *
    * Subsequent repetitions of these last two lines would
    * simply result in the c value being incremented again
    * and again.
    *
    * @param aKeys the Array of keys to chain Objects together with.
    * @param aEndWith the value to assign to the last key.
+   * @param aRoot the root object onto which we create/get the object chain
+   *              designated by aKeys.
    * @returns a reference to the second last object in the chain -
    *          so in our example, that'd be "b".
    */
-  _ensureObjectChain: function(aKeys, aEndWith) {
-    let current = this._countableEvents;
+  _ensureObjectChain: function(aKeys, aEndWith, aRoot) {
+    let current = aRoot;
     let parent = null;
     aKeys.unshift(this._bucket);
     for (let [i, key] of Iterator(aKeys)) {
       if (!(key in current)) {
         if (i == aKeys.length - 1) {
           current[key] = aEndWith;
         } else {
           current[key] = {};
@@ -237,18 +241,18 @@ this.BrowserUITelemetry = {
       }
       parent = current;
       current = current[key];
     }
     return parent;
   },
 
   _countableEvents: {},
-  _countEvent: function(aKeyArray) {
-    let countObject = this._ensureObjectChain(aKeyArray, 0);
+  _countEvent: function(aKeyArray, root=this._countableEvents) {
+    let countObject = this._ensureObjectChain(aKeyArray, 0, root);
     let lastItemKey = aKeyArray[aKeyArray.length - 1];
     countObject[lastItemKey]++;
   },
 
   _countMouseUpEvent: function(aCategory, aAction, aButton) {
     const BUTTONS = ["left", "middle", "right"];
     let buttonKey = BUTTONS[aButton];
     if (buttonKey) {
@@ -582,16 +586,64 @@ this.BrowserUITelemetry = {
       this._durations.customization.push({
         duration: duration,
         bucket: durationMap.customization.bucket,
       });
       delete durationMap.customization;
     }
   },
 
+  _contextMenuItemWhitelist: new Set([
+    "close-without-interaction", // for closing the menu without clicking it.
+    "custom-page-item", // The ID we use for page-provided items
+    "unknown", // The bucket for stuff with no id.
+    // Everything we know of so far (which will exclude add-on items):
+    "navigation", "back", "forward", "reload", "stop", "bookmarkpage",
+    "spell-no-suggestions", "spell-add-to-dictionary",
+    "spell-undo-add-to-dictionary", "openlinkincurrent", "openlinkintab",
+    "openlink", "openlinkprivate", "bookmarklink", "sharelink", "savelink",
+    "marklinkMenu", "copyemail", "copylink", "media-play", "media-pause",
+    "media-mute", "media-unmute", "media-playbackrate",
+    "media-playbackrate-050x", "media-playbackrate-100x",
+    "media-playbackrate-150x", "media-playbackrate-200x",
+    "media-showcontrols", "media-hidecontrols", "video-showstats",
+    "video-hidestats", "video-fullscreen", "leave-dom-fullscreen",
+    "reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage",
+    "copyvideourl", "copyaudiourl", "saveimage", "shareimage", "sendimage",
+    "setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo",
+    "sharevideo", "saveaudio", "video-saveimage", "sendvideo", "sendaudio",
+    "ctp-play", "ctp-hide", "sharepage", "savepage", "markpageMenu",
+    "viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall",
+    "keywordfield", "searchselect", "shareselect", "frame", "showonlythisframe",
+    "openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe",
+    "printframe", "viewframesource", "viewframeinfo",
+    "viewpartialsource-selection", "viewpartialsource-mathml",
+    "viewsource", "viewinfo", "spell-check-enabled",
+    "spell-add-dictionaries-main", "spell-dictionaries",
+    "spell-dictionaries-menu", "spell-add-dictionaries",
+    "bidi-text-direction-toggle", "bidi-page-direction-toggle", "inspect",
+  ]),
+
+  _contextMenuInteractions: {},
+
+  registerContextMenuInteraction: function(keys, itemID) {
+    if (itemID) {
+      if (!this._contextMenuItemWhitelist.has(itemID)) {
+        itemID = "other-item";
+      }
+      keys.push(itemID);
+    }
+
+    this._countEvent(keys, this._contextMenuInteractions);
+  },
+
+  getContextMenuInfo: function() {
+    return this._contextMenuInteractions;
+  },
+
   _bucket: BUCKET_DEFAULT,
   _bucketTimer: null,
 
   /**
    * Default bucket name, when no other bucket is active.
    */
   get BUCKET_DEFAULT() BUCKET_DEFAULT,
 
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -89,22 +89,23 @@
 }
 
 #customization-palette,
 #customization-empty {
   padding: 0 25px 25px;
 }
 
 #customization-header {
-  font-size: 1.5em;
-  line-height: 1.5em;
-  color: rgb(64,100,128);
-  font-weight: lighter;
-  margin-bottom: 1em;
-  padding: 25px 25px 0;
+  font-size: 1.75em;
+  line-height: 1.75em;
+  color: #666;
+  font-weight: 200;
+  margin: 25px 25px 12px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #e5e5e5;
 }
 
 #customization-panel-container {
   padding: 15px 25px 25px;
   background-image: linear-gradient(to bottom, #3e86ce, #3878ba);
 }
 
 #main-window:-moz-any([customize-entering],[customize-entered]) #browser-bottombox,
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -2512,16 +2512,19 @@ public class BrowserApp extends GeckoApp
                 url = urlFromReader;
             }
         }
 
         // Disable share menuitem for about:, chrome:, file:, and resource: URIs
         final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
         share.setVisible(!inGuestMode);
         share.setEnabled(StringUtils.isShareableUrl(url) && !inGuestMode);
+        MenuUtils.safeSetEnabled(aMenu, R.id.apps, !inGuestMode);
+        MenuUtils.safeSetEnabled(aMenu, R.id.addons, !inGuestMode);
+        MenuUtils.safeSetEnabled(aMenu, R.id.downloads, !inGuestMode);
 
         // NOTE: Use MenuUtils.safeSetEnabled because some actions might
         // be on the BrowserToolbar context menu
         MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
         MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
         MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
         MenuUtils.safeSetEnabled(aMenu, R.id.site_settings, !isAboutHome(tab));
         MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, !isAboutHome(tab));
--- a/mobile/android/locales/en-US/chrome/webapp.properties
+++ b/mobile/android/locales/en-US/chrome/webapp.properties
@@ -50,8 +50,10 @@ installUpdateMessage2=Touch to install u
 # example: 3 updates failed
 retrievalFailedTitle=#1 update failed;#1 updates failed
 
 # LOCALIZATION NOTE (retrievalFailedMessage): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # %1$S is a comma-separated list of apps for which retrieval failed.
 # example: Failed to retrieve updates for Foo, Bar, Baz
 retrievalFailedMessage=Failed to retrieve update for %1$S;Failed to retrieve updates for %1$S
+
+webappsDisabledInGuest=Installing apps is disabled in guest sessions
--- a/mobile/android/modules/WebappManager.jsm
+++ b/mobile/android/modules/WebappManager.jsm
@@ -84,16 +84,22 @@ this.WebappManager = {
       DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager);
       return;
     }
 
     this._installApk(aMessage, aMessageManager);
   },
 
   _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
+    if (this.inGuestSession()) {
+      aMessage.error = Strings.GetStringFromName("webappsDisabledInGuest"),
+      aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
+      return;
+    }
+
     let filePath;
 
     try {
       filePath = yield this._downloadApk(aMessage.app.manifestURL);
     } catch(ex) {
       aMessage.error = ex;
       aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
       debug("error downloading APK: " + ex);
@@ -252,16 +258,20 @@ this.WebappManager = {
       // to ensure the user can always remove an app from the registry (and thus
       // about:apps) even if it's out of sync with installed APKs.
       debug("APK not installed; proceeding directly to removal from registry");
       DOMApplicationRegistry.doUninstall(aData, aMessageManager);
     }
 
   }),
 
+  inGuestSession: function() {
+    return Services.wm.getMostRecentWindow("navigator:browser").BrowserApp.isGuest;
+  },
+
   autoInstall: function(aData) {
     debug("autoInstall " + aData.manifestURL);
 
     // If the app is already installed, update the existing installation.
     // We should be able to use DOMApplicationRegistry.getAppByManifestURL,
     // but it returns a mozIApplication, while _autoUpdate needs the original
     // object from DOMApplicationRegistry.webapps in order to modify it.
     for (let [ , app] in Iterator(DOMApplicationRegistry.webapps)) {
--- a/python/mozboot/mozboot/bootstrap.py
+++ b/python/mozboot/mozboot/bootstrap.py
@@ -54,17 +54,17 @@ class Bootstrapper(object):
                 # Most Linux Mint editions are based on Ubuntu; one is based on
                 # Debian, and reports this in dist_id
                 if dist_id == 'debian':
                     cls = DebianBootstrapper
                 else:
                     cls = UbuntuBootstrapper
             elif distro == 'Ubuntu':
                 cls = UbuntuBootstrapper
-            elif distro == 'Elementary':
+            elif distro in ('Elementary OS', 'Elementary'):
                 cls = UbuntuBootstrapper
             else:
                 raise NotImplementedError('Bootstrap support for this Linux '
                                           'distro not yet available.')
 
             args['version'] = version
             args['dist_id'] = dist_id
 
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1960,39 +1960,54 @@ ThreadActor.prototype = {
   createValueGrip: function (aValue, aPool=false) {
     if (!aPool) {
       aPool = this._pausePool;
     }
 
     switch (typeof aValue) {
       case "boolean":
         return aValue;
+
       case "string":
         if (this._stringIsLong(aValue)) {
           return this.longStringGrip(aValue, aPool);
         }
         return aValue;
+
       case "number":
         if (aValue === Infinity) {
           return { type: "Infinity" };
         } else if (aValue === -Infinity) {
           return { type: "-Infinity" };
         } else if (Number.isNaN(aValue)) {
           return { type: "NaN" };
         } else if (!aValue && 1 / aValue === -Infinity) {
           return { type: "-0" };
         }
         return aValue;
+
       case "undefined":
         return { type: "undefined" };
+
       case "object":
         if (aValue === null) {
           return { type: "null" };
         }
         return this.objectGrip(aValue, aPool);
+
+      case "symbol":
+        let form = {
+          type: "symbol"
+        };
+        let name = getSymbolName(aValue);
+        if (name !== undefined) {
+          form.name = this.createValueGrip(name);
+        }
+        return form;
+
       default:
         dbg_assert(false, "Failed to provide a grip for: " + aValue);
         return null;
     }
   },
 
   /**
    * Return a protocol completion value representing the given
@@ -5425,16 +5440,23 @@ function makeDebuggeeValueIfNeeded(obj, 
   return value;
 }
 
 function getInnerId(window) {
   return window.QueryInterface(Ci.nsIInterfaceRequestor).
                 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 };
 
+const symbolProtoToString = Symbol.prototype.toString;
+
+function getSymbolName(symbol) {
+  const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1);
+  return name || undefined;
+}
+
 exports.register = function(handle) {
   ThreadActor.breakpointStore = new BreakpointStore();
   ThreadSources._blackBoxedSources = new Set(["self-hosted"]);
   ThreadSources._prettyPrintedSources = new Map();
 };
 
 exports.unregister = function(handle) {
   ThreadActor.breakpointStore = null;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_symbols-01.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can represent ES6 Symbols over the RDP.
+ */
+
+const URL = "foo.js";
+
+function run_test() {
+  initTestDebuggerServer();
+  const debuggee = addTestGlobal("test-symbols");
+  const client = new DebuggerClient(DebuggerServer.connectPipe());
+
+  client.connect(function() {
+    attachTestTabAndResume(client, "test-symbols", function(response, tabClient, threadClient) {
+      add_task(testSymbols.bind(null, client, debuggee));
+      run_next_test();
+    });
+  });
+
+  do_test_pending();
+}
+
+function* testSymbols(client, debuggee) {
+  const evalCode = () => {
+    Components.utils.evalInSandbox(
+      "(" + function () {
+        var symbolWithName = Symbol("Chris");
+        var symbolWithoutName = Symbol();
+        var iteratorSymbol = Symbol.iterator;
+        debugger;
+      } + "())",
+      debuggee,
+      "1.8",
+      URL,
+      1
+    );
+  };
+
+  const packet = yield executeOnNextTickAndWaitForPause(evalCode, client);
+  const {
+    symbolWithName,
+    symbolWithoutName,
+    iteratorSymbol
+  } = packet.frame.environment.bindings.variables;
+
+  equal(symbolWithName.value.type, "symbol");
+  equal(symbolWithName.value.name, "Chris");
+
+  equal(symbolWithoutName.value.type, "symbol");
+  ok(!("name" in symbolWithoutName.value));
+
+  equal(iteratorSymbol.value.type, "symbol");
+  equal(iteratorSymbol.value.name, "Symbol.iterator");
+
+  finishClient(client);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_symbols-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we don't run debuggee code when getting symbol names.
+ */
+
+const URL = "foo.js";
+
+function run_test() {
+  initTestDebuggerServer();
+  const debuggee = addTestGlobal("test-symbols");
+  const client = new DebuggerClient(DebuggerServer.connectPipe());
+
+  client.connect(function() {
+    attachTestTabAndResume(client, "test-symbols", function(response, tabClient, threadClient) {
+      add_task(testSymbols.bind(null, client, debuggee));
+      run_next_test();
+    });
+  });
+
+  do_test_pending();
+}
+
+function* testSymbols(client, debuggee) {
+  const evalCode = () => {
+    Components.utils.evalInSandbox(
+      "(" + function () {
+        Symbol.prototype.toString = () => {
+          throw new Error("lololol");
+        };
+        var sym = Symbol("le troll");
+        debugger;
+      } + "())",
+      debuggee,
+      "1.8",
+      URL,
+      1
+    );
+  };
+
+  const packet = yield executeOnNextTickAndWaitForPause(evalCode, client);
+  const { sym } = packet.frame.environment.bindings.variables;
+
+  equal(sym.value.type, "symbol");
+  equal(sym.value.name, "le troll");
+
+  finishClient(client);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -200,8 +200,10 @@ reason = bug 820380
 [test_ignore_caught_exceptions.js]
 [test_requestTypes.js]
 reason = bug 937197
 [test_layout-reflows-observer.js]
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_monitor_actor.js]
+[test_symbols-01.js]
+[test_symbols-02.js]