Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 01 Apr 2016 15:43:36 -0700
changeset 291402 c40c0b2f3b4c778af4307e090b4063b63c806cda
parent 291291 b6ea6a3bb8a6fc355b46403919d8c70e798c7007 (current diff)
parent 291401 543ba2e092d2d4de86b1994a646ee75df5519c58 (diff)
child 291437 c410d4e20586be94bbddb63e2cb46258be842d37
child 291463 57626d2359e0b625a7cdbb6eab381200ec0ebaf8
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
Merge fx-team to central, a=merge MozReview-Commit-ID: 8rupfWq5Wa6
devtools/client/responsive.html/test/browser/browser_devices.json
mobile/android/base/moz.build
mobile/android/base/resources/xml-v11/preferences_general.xml
mobile/android/base/resources/xml-v11/preferences_general_tablet.xml
toolkit/components/extensions/ext-cookies.js
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -468,17 +468,17 @@
                    onpopupshowing="if (!this.parentNode._placesView)
                                      new PlacesMenu(event, 'place:folder=TOOLBAR');"/>
       </menu>
       <menuseparator id="bookmarksMenuItemsSeparator"/>
       <!-- Bookmarks menu items -->
       <menuseparator builder="end"
                      class="hide-if-empty-places-result"/>
       <menuitem id="menu_unsortedBookmarks"
-                label="&unsortedBookmarksCmd.label;"
+                label="&otherBookmarksCmd.label;"
                 oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/>
     </menupopup>
   </menu>
 
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
                   accesskey="&toolsMenu.accesskey;"
                   onpopupshowing="mirrorShow(this)">
@@ -515,59 +515,19 @@
                         accesskey="&syncReAuthItem.accesskey;"
                         observes="sync-reauth-state"
                         oncommand="gSyncUI.openSignInAgainPage('menubar');"/>
               <menuseparator id="devToolsSeparator"/>
               <menu id="webDeveloperMenu"
                     label="&webDeveloperMenu.label;"
                     accesskey="&webDeveloperMenu.accesskey;">
                 <menupopup id="menuWebDeveloperPopup">
-                  <menuitem id="menu_devToolbox"
-                            observes="devtoolsMenuBroadcaster_DevToolbox"
-                            accesskey="&devToolboxMenuItem.accesskey;"/>
-                  <menuseparator id="menu_devtools_separator"/>
-                  <menuitem id="menu_devToolbar"
-                            observes="devtoolsMenuBroadcaster_DevToolbar"
-                            accesskey="&devToolbarMenu.accesskey;"/>
-                  <menuitem id="menu_webide"
-                            observes="devtoolsMenuBroadcaster_webide"
-                            accesskey="&webide.accesskey;"/>
-                  <menuitem id="menu_browserToolbox"
-                            observes="devtoolsMenuBroadcaster_BrowserToolbox"
-                            accesskey="&browserToolboxMenu.accesskey;"/>
-                  <menuitem id="menu_browserContentToolbox"
-                            observes="devtoolsMenuBroadcaster_BrowserContentToolbox"
-                            accesskey="&browserContentToolboxMenu.accesskey;" />
-                  <menuitem id="menu_browserConsole"
-                            observes="devtoolsMenuBroadcaster_BrowserConsole"
-                            accesskey="&browserConsoleCmd.accesskey;"/>
-                  <menuitem id="menu_responsiveUI"
-                            observes="devtoolsMenuBroadcaster_ResponsiveUI"
-                            accesskey="&responsiveDesignMode.accesskey;"/>
-                  <menuitem id="menu_eyedropper"
-                            observes="devtoolsMenuBroadcaster_Eyedropper"
-                            accesskey="&eyedropper.accesskey;"/>
-                  <menuitem id="menu_scratchpad"
-                            observes="devtoolsMenuBroadcaster_Scratchpad"
-                            accesskey="&scratchpad.accesskey;"/>
                   <menuitem id="menu_pageSource"
                             observes="devtoolsMenuBroadcaster_PageSource"
                             accesskey="&pageSourceCmd.accesskey;"/>
-                  <menuitem id="javascriptConsole"
-                            observes="devtoolsMenuBroadcaster_ErrorConsole"
-                            accesskey="&errorConsoleCmd.accesskey;"/>
-                  <menuitem id="menu_devtools_serviceworkers"
-                            observes="devtoolsMenuBroadcaster_ServiceWorkers"
-                            accesskey="&devtoolsServiceWorkers.accesskey;"/>
-                  <menuitem id="menu_devtools_connect"
-                            observes="devtoolsMenuBroadcaster_connect"/>
-                  <menuseparator id="devToolsEndSeparator"/>
-                  <menuitem id="getMoreDevtools"
-                            observes="devtoolsMenuBroadcaster_GetMoreTools"
-                            accesskey="&getMoreDevtoolsCmd.accesskey;"/>
                 </menupopup>
               </menu>
               <menuitem id="menu_pageInfo"
                         accesskey="&pageInfoCmd.accesskey;"
                         label="&pageInfoCmd.label;"
 #ifndef XP_WIN
                         key="key_viewInfo"
 #endif
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -90,30 +90,17 @@
     <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
     <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/>
     <command id="Browser:OpenLocation" oncommand="openLocation();"/>
     <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/>
     <command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);" reserved="true"/>
 
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
-    <command id="Tools:DevToolbox" oncommand="gDevToolsBrowser.toggleToolboxCommand(gBrowser);"/>
-    <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/>
-    <command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/>
-    <command id="Tools:WebIDE" oncommand="gDevToolsBrowser.openWebIDE();" disabled="true" hidden="true"/>
-    <command id="Tools:BrowserToolbox" oncommand="BrowserToolboxProcess.init();" disabled="true" hidden="true"/>
-    <command id="Tools:BrowserContentToolbox" oncommand="gDevToolsBrowser.openContentProcessToolbox();" disabled="true" hidden="true"/>
-    <command id="Tools:BrowserConsole" oncommand="HUDService.openBrowserConsoleOrFocus();"/>
-    <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();"/>
-    <command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();"/>
-    <command id="Tools:Eyedropper" oncommand="openEyedropper();"/>
     <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
-    <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/>
-    <command id="Tools:ServiceWorkers" oncommand="gDevToolsBrowser.openAboutDebugging(gBrowser, 'workers')"/>
-    <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/>
     <command id="Tools:Sanitize"
      oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
     <command id="Tools:PrivateBrowsing"
       oncommand="OpenBrowserWindow({private: true});" reserved="true"/>
 #ifdef E10S_TESTING_ONLY
     <command id="Tools:NonRemoteWindow"
       oncommand="OpenBrowserWindow({remote: false});"/>
 #endif
@@ -191,73 +178,22 @@
     <broadcaster id="sync-reauth-state" hidden="true"/>
     <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
                  type="checkbox" group="sidebar"
                  sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
                  oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
     <broadcaster id="workOfflineMenuitemState"/>
     <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
 
-    <!-- DevTools broadcasters -->
-    <broadcaster id="devtoolsMenuBroadcaster_DevToolbox"
-                 label="&devToolboxMenuItem.label;"
-                 type="checkbox" autocheck="false"
-                 command="Tools:DevToolbox"
-                 key="key_devToolboxMenuItem"/>
-    <broadcaster id="devtoolsMenuBroadcaster_DevToolbar"
-                 label="&devToolbarMenu.label;"
-                 type="checkbox" autocheck="false"
-                 command="Tools:DevToolbar"
-                 key="key_devToolbar"/>
-    <broadcaster id="devtoolsMenuBroadcaster_webide"
-                 label="&webide.label;"
-                 command="Tools:WebIDE"
-                 key="key_webide"/>
-    <broadcaster id="devtoolsMenuBroadcaster_BrowserToolbox"
-                 label="&browserToolboxMenu.label;"
-                 key="key_browserToolbox"
-                 command="Tools:BrowserToolbox"/>
-    <broadcaster id="devtoolsMenuBroadcaster_BrowserContentToolbox"
-                 label="&browserContentToolboxMenu.label;"
-                 command="Tools:BrowserContentToolbox"/>
-    <broadcaster id="devtoolsMenuBroadcaster_BrowserConsole"
-                 label="&browserConsoleCmd.label;"
-                 key="key_browserConsole"
-                 command="Tools:BrowserConsole"/>
-    <broadcaster id="devtoolsMenuBroadcaster_Scratchpad"
-                 label="&scratchpad.label;"
-                 command="Tools:Scratchpad"
-                 key="key_scratchpad"/>
-    <broadcaster id="devtoolsMenuBroadcaster_ResponsiveUI"
-                 label="&responsiveDesignMode.label;"
-                 type="checkbox" autocheck="false"
-                 command="Tools:ResponsiveUI"
-                 key="key_responsiveUI"/>
-    <broadcaster id="devtoolsMenuBroadcaster_Eyedropper"
-                 label="&eyedropper.label;"
-                 type="checkbox" autocheck="false"
-                 command="Tools:Eyedropper"/>
     <broadcaster id="devtoolsMenuBroadcaster_PageSource"
                  label="&pageSourceCmd.label;"
                  key="key_viewSource"
                  command="View:PageSource">
       <observes element="canViewSource" attribute="disabled"/>
     </broadcaster>
-    <broadcaster id="devtoolsMenuBroadcaster_ErrorConsole"
-                 label="&errorConsoleCmd.label;"
-                 command="Tools:ErrorConsole"/>
-    <broadcaster id="devtoolsMenuBroadcaster_GetMoreTools"
-                 label="&getMoreDevtoolsCmd.label;"
-                 oncommand="openUILinkIn('https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/', 'tab');"/>
-    <broadcaster id="devtoolsMenuBroadcaster_ServiceWorkers"
-                 label="&devtoolsServiceWorkers.label;"
-                 command="Tools:ServiceWorkers"/>
-    <broadcaster id="devtoolsMenuBroadcaster_connect"
-                 label="&devtoolsConnect.label;"
-                 command="Tools:DevToolsConnect"/>
   </broadcasterset>
 
   <keyset id="mainKeyset">
     <key id="key_newNavigator"
          key="&newNavigatorCmd.key;"
          command="cmd_newNavigator"
          modifiers="accel"/>
     <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTab"/>
@@ -294,41 +230,16 @@
 #endif
 #ifdef XP_GNOME
     <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/>
     <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/>
 #else
     <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/>
 #endif
     <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/>
-    <key id="key_devToolboxMenuItemF12" keycode="&devToolsCmd.keycode;" keytext="&devToolsCmd.keytext;" command="Tools:DevToolbox"/>
-    <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/>
-    <key id="key_browserToolbox" key="&browserToolboxCmd.commandkey;" command="Tools:BrowserToolbox" modifiers="accel,alt,shift"/>
-    <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift"
-         keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/>
-    <key id="key_responsiveUI" key="&responsiveDesignMode.commandkey;" command="Tools:ResponsiveUI"
-#ifdef XP_MACOSX
-        modifiers="accel,alt"
-#else
-        modifiers="accel,shift"
-#endif
-    />
-    <key id="key_webide" keycode="&webide.keycode;" command="Tools:WebIDE"
-         modifiers="shift" keytext="&webide.keytext;"/>
-    <key id="key_devToolboxMenuItem" keytext="&devToolboxMenuItem.keytext;"
-         command="Tools:DevToolbox" key="&devToolboxMenuItem.keytext;"
-#ifdef XP_MACOSX
-        modifiers="accel,alt"
-#else
-        modifiers="accel,shift"
-#endif
-    />
-
-    <key id="key_scratchpad" keycode="&scratchpad.keycode;" modifiers="shift"
-         keytext="&scratchpad.keytext;" command="Tools:Scratchpad"/>
     <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile"  modifiers="accel"/>
     <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/>
     <key id="printKb" key="&printCmd.commandkey;" command="cmd_print"  modifiers="accel"/>
     <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
     <key id="key_closeWindow" key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/>
     <key id="key_toggleMute" key="&toggleMuteCmd.key;" command="cmd_toggleMute" modifiers="control"/>
     <key id="key_undo"
          key="&undoCmd.key;"
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -855,17 +855,17 @@
                           oncommand="onViewToolbarCommand(event)"
                           label="&viewBookmarksToolbar.label;"/>
                 <menuseparator/>
                 <!-- Bookmarks toolbar items -->
               </menupopup>
             </menu>
             <menu id="BMB_unsortedBookmarks"
                   class="menu-iconic bookmark-item subviewbutton"
-                  label="&bookmarksMenuButton.unsorted.label;"
+                  label="&bookmarksMenuButton.other.label;"
                   container="true">
               <menupopup id="BMB_unsortedBookmarksPopup"
                          placespopup="true"
                          context="placesContext"
                          onpopupshowing="if (!this.parentNode._placesView)
                                            new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS',
                                                           PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
             </menu>
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -210,17 +210,17 @@
                        class="subviewbutton"
                        oncommand="onViewToolbarCommand(event); PanelUI.hide();"/>
         <toolbarseparator/>
         <toolbarbutton id="panelMenu_bookmarksToolbar"
                        label="&personalbarCmd.label;"
                        class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/>
         <toolbarbutton id="panelMenu_unsortedBookmarks"
-                       label="&unsortedBookmarksCmd.label;"
+                       label="&otherBookmarksCmd.label;"
                        class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
         <toolbarseparator class="small-separator"/>
         <toolbaritem id="panelMenu_bookmarksMenu"
                      orient="vertical"
                      smoothscroll="false"
                      onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -15,19 +15,16 @@ var {
 // Map[Extension -> Map[ID -> MenuItem]]
 // Note: we want to enumerate all the menu items so
 // this cannot be a weak map.
 var gContextMenuMap = new Map();
 
 // Map[Extension -> MenuItem]
 var gRootItems = new Map();
 
-// Not really used yet, will be used for event pages.
-var gOnClickedCallbacksMap = new WeakMap();
-
 // If id is not specified for an item we use an integer.
 var gNextMenuItemID = 0;
 
 // Used to assign unique names to radio groups.
 var gNextRadioGroupID = 0;
 
 // The max length of a menu item's label.
 var gMaxLabelLength = 64;
@@ -163,33 +160,39 @@ var gMenuBuilder = {
       }
     }
 
     if (!item.enabled) {
       element.setAttribute("disabled", "true");
     }
 
     element.addEventListener("command", event => {  // eslint-disable-line mozilla/balanced-listeners
+      if (event.target !== event.currentTarget) {
+        return;
+      }
       if (item.type == "checkbox") {
         item.checked = !item.checked;
       } else if (item.type == "radio") {
         // Deselect all radio items in the current radio group.
         for (let child of item.parent.children) {
           if (child.type == "radio" && child.groupName == item.groupName) {
             child.checked = false;
           }
         }
         // Select the clicked radio item.
         item.checked = true;
       }
 
       item.tabManager.addActiveTabPermission();
+
+      let tab = item.tabManager.convert(contextData.tab);
+      let info = item.getClickInfo(contextData, event);
+      item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
       if (item.onclick) {
-        let clickData = item.getClickData(contextData, event);
-        runSafe(item.extContext, item.onclick, clickData);
+        runSafe(item.extContext, item.onclick, info, tab);
       }
     });
 
     return element;
   },
 
   handleEvent: function(event) {
     if (this.xulMenu != event.target || event.type != "popuphidden") {
@@ -385,52 +388,48 @@ MenuItem.prototype = {
 
     let menuMap = gContextMenuMap.get(this.extension);
     menuMap.delete(this.id);
     if (this.root == this) {
       gRootItems.delete(this.extension);
     }
   },
 
-  getClickData(contextData, event) {
+  getClickInfo(contextData, event) {
     let mediaType;
     if (contextData.onVideo) {
       mediaType = "video";
     }
     if (contextData.onAudio) {
       mediaType = "audio";
     }
     if (contextData.onImage) {
       mediaType = "image";
     }
 
-    let clickData = {
+    let info = {
       menuItemId: this.id,
     };
 
     function setIfDefined(argName, value) {
       if (value) {
-        clickData[argName] = value;
+        info[argName] = value;
       }
     }
 
-    let tab = contextData.tab ? TabManager.convert(this.extension, contextData.tab)
-                              : undefined;
-
     setIfDefined("parentMenuItemId", this.parentId);
     setIfDefined("mediaType", mediaType);
     setIfDefined("linkUrl", contextData.linkUrl);
     setIfDefined("srcUrl", contextData.srcUrl);
     setIfDefined("pageUrl", contextData.pageUrl);
     setIfDefined("frameUrl", contextData.frameUrl);
     setIfDefined("selectionText", contextData.selectionText);
     setIfDefined("editable", contextData.onEditableArea);
-    setIfDefined("tab", tab);
 
-    return clickData;
+    return info;
   },
 
   enabledForContext(contextData) {
     let contexts = getContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
@@ -502,22 +501,21 @@ extensions.registerSchemaAPI("contextMen
       removeAll: function() {
         let root = gRootItems.get(extension);
         if (root) {
           root.remove();
         }
         return Promise.resolve();
       },
 
-      // TODO: implement this once event pages are ready.
       onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
-        let callback = menuItem => {
-          fire(menuItem.data);
+        let listener = (event, info, tab) => {
+          fire(info, tab);
         };
 
-        gOnClickedCallbacksMap.set(extension, callback);
+        extension.on("webext-contextmenu-menuitem-click", listener);
         return () => {
-          gOnClickedCallbacksMap.delete(extension);
+          extension.off("webext-contextmenu-menuitem-click", listener);
         };
       }).api(),
     },
   };
 });
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -537,24 +537,16 @@ extensions.registerSchemaAPI("tabs", nul
       getCurrent() {
         let tab;
         if (context.tabId) {
           tab = TabManager.convert(extension, TabManager.getTab(context.tabId));
         }
         return Promise.resolve(tab);
       },
 
-      getAllInWindow: function(windowId) {
-        if (windowId === null) {
-          windowId = WindowManager.topWindow.windowId;
-        }
-
-        return self.tabs.query({windowId});
-      },
-
       query: function(queryInfo) {
         let pattern = null;
         if (queryInfo.url !== null) {
           if (!extension.hasPermission("tabs")) {
             return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
           }
 
           pattern = new MatchPattern(queryInfo.url);
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -313,18 +313,19 @@
             "parameters": [
               {"name": "tab", "$ref": "Tab"}
             ]
           }
         ]
       },
       {
         "name": "getAllInWindow",
+        "deprecated": "Please use $(ref:tabs.query) <code>{windowId: windowId}</code>.",
+        "unsupported": true,
         "type": "function",
-        "deprecated": "Please use $(ref:tabs.query) <code>{windowId: windowId}</code>.",
         "description": "Gets details about all tabs in the specified window.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "windowId",
             "minimum": -2,
             "optional": true,
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -12,20 +12,24 @@ add_task(function* () {
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["contextMenus"],
     },
 
     background: function() {
       // A generic onclick callback function.
-      function genericOnClick(info) {
-        browser.test.sendMessage("menuItemClick", info);
+      function genericOnClick(info, tab) {
+        browser.test.sendMessage("onclick", {info, tab});
       }
 
+      browser.contextMenus.onClicked.addListener((info, tab) => {
+        browser.test.sendMessage("browser.contextMenus.onClicked", {info, tab});
+      });
+
       browser.contextMenus.create({
         contexts: ["all"],
         type: "separator",
       });
 
       let contexts = ["page", "selection", "image"];
       for (let i = 0; i < contexts.length; i++) {
         let context = contexts[i];
@@ -34,19 +38,19 @@ add_task(function* () {
           title: title,
           contexts: [context],
           id: "ext-" + context,
           onclick: genericOnClick,
         });
         if (context == "selection") {
           browser.contextMenus.update("ext-selection", {
             title: "selection is: '%s'",
-            onclick: (info) => {
+            onclick: (info, tab) => {
               browser.contextMenus.removeAll();
-              genericOnClick(info);
+              genericOnClick(info, tab);
             },
           });
         }
       }
 
       let parent = browser.contextMenus.create({
         title: "parent",
       });
@@ -75,60 +79,59 @@ add_task(function* () {
         onclick: genericOnClick,
       });
       browser.contextMenus.remove(parentToDel);
 
       browser.contextMenus.create({
         title: "radio-group-1",
         type: "radio",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         type: "separator",
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
+      browser.contextMenus.create({
+        title: "Without onclick property",
+        id: "ext-without-onclick",
+      });
+
       browser.contextMenus.update(parent, {parentId: child2}).then(
         () => {
           browser.test.notifyFail();
         },
         () => {
           browser.test.notifyPass();
         }
       );
@@ -157,30 +160,39 @@ add_task(function* () {
     }, gBrowser.selectedBrowser);
     yield popupShownPromise;
 
     popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
     EventUtils.synthesizeMouseAtCenter(getTop(), {});
     yield popupShownPromise;
   }
 
-  function* closeContextMenu(itemToSelect, expectedClickInfo) {
-    function checkClickInfo(info) {
+  function* closeContextMenu(itemToSelect, expectedClickInfo, hasOnclickProperty = true) {
+    function checkClickInfo(info, tab) {
       for (let i of Object.keys(expectedClickInfo)) {
         is(info[i], expectedClickInfo[i],
            "click info " + i + " expected to be: " + expectedClickInfo[i] + " but was: " + info[i]);
       }
-      is(expectedClickInfo.pageSrc, info.tab.url);
+      is(expectedClickInfo.pageSrc, tab.url);
     }
     let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
     EventUtils.synthesizeMouseAtCenter(itemToSelect, {});
-    let clickInfo = yield extension.awaitMessage("menuItemClick");
+
+    if (hasOnclickProperty) {
+      let {info, tab} = yield extension.awaitMessage("onclick");
+      if (expectedClickInfo) {
+        checkClickInfo(info, tab);
+      }
+    }
+
+    let {info, tab} = yield extension.awaitMessage("browser.contextMenus.onClicked");
     if (expectedClickInfo) {
-      checkClickInfo(clickInfo);
+      checkClickInfo(info, tab);
     }
+
     yield popupHiddenPromise;
   }
 
   function confirmRadioGroupStates(expectedStates) {
     let top = getTop();
 
     let radioItems = top.getElementsByAttribute("type", "radio");
     let radioGroup1 = top.getElementsByAttribute("label", "radio-group-1");
@@ -283,16 +295,29 @@ add_task(function* () {
     selection.addRange(range);
   });
 
   // Bring up context menu again
   yield openExtensionMenu();
 
   // Check some menu items
   top = getTop();
+  items = top.getElementsByAttribute("label", "Without onclick property");
+  is(items.length, 1, "contextMenu item was found (context=page)");
+
+  yield closeContextMenu(items[0], {
+    menuItemId: "ext-without-onclick",
+    pageUrl: "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html",
+  }, false /* hasOnclickProperty */);
+
+  // Bring up context menu again
+  yield openExtensionMenu();
+
+  // Check some menu items
+  top = getTop();
   items = top.getElementsByAttribute("label", "selection is: 'just some text 123456789012345678901234567890...'");
   is(items.length, 1, "contextMenu item for selection was found (context=selection)");
   let selectionItem = items[0];
 
   items = top.getElementsByAttribute("label", "selection");
   is(items.length, 0, "contextMenu item label update worked (context=selection)");
 
   yield closeContextMenu(selectionItem, {
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -955,17 +955,17 @@ this.PlacesUIUtils = {
           concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"),
           concreteId: PlacesUtils.toolbarFolderId },
       "BookmarksMenu":
         { title: null,
           concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"),
           concreteId: PlacesUtils.bookmarksMenuFolderId },
       "UnfiledBookmarks":
         { title: null,
-          concreteTitle: PlacesUtils.getString("UnsortedBookmarksFolderTitle"),
+          concreteTitle: PlacesUtils.getString("OtherBookmarksFolderTitle"),
           concreteId: PlacesUtils.unfiledBookmarksFolderId },
     };
     // All queries but PlacesRoot.
     const EXPECTED_QUERY_COUNT = 7;
 
     // Removes an item and associated annotations, ignoring eventual errors.
     function safeRemoveItem(aItemId) {
       try {
--- a/browser/components/places/tests/unit/bookmarks.glue.json
+++ b/browser/components/places/tests/unit/bookmarks.glue.json
@@ -1,1 +1,1 @@
-{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]}
+{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Other Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]}
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -1,17 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
   image.png
   uitour.html
   ../UITour-lib.js
 
 [browser_backgroundTab.js]
-skip-if = e10s # Intermittent failures, bug 1244991
 [browser_closeTab.js]
 [browser_fxa.js]
 skip-if = debug || asan # updateAppMenuItem leaks
 [browser_no_tabs.js]
 [browser_openPreferences.js]
 [browser_openSearchPanel.js]
 skip-if = true # Bug 1113038 - Intermittent "Popup was opened"
 [browser_trackingProtection.js]
--- a/browser/components/uitour/test/browser_backgroundTab.js
+++ b/browser/components/uitour/test/browser_backgroundTab.js
@@ -2,33 +2,45 @@
 
 var gTestTab;
 var gContentAPI;
 var gContentWindow;
 
 requestLongerTimeout(2);
 add_task(setup_UITourTest);
 
-
 add_UITour_task(function* test_bg_getConfiguration() {
   info("getConfiguration is on the allowed list so should work");
   yield* loadForegroundTab();
   let data = yield getConfigurationPromise("availableTargets");
   ok(data, "Got data from getConfiguration");
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 add_UITour_task(function* test_bg_showInfo() {
   info("showInfo isn't on the allowed action list so should be denied");
   yield* loadForegroundTab();
 
-  yield showInfoPromise("appMenu", "Hello from the backgrund", "Surprise!").then(
+  yield showInfoPromise("appMenu", "Hello from the background", "Surprise!").then(
     () => ok(false, "panel shouldn't have shown from a background tab"),
     () => ok(true, "panel wasn't shown from a background tab"));
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 
 function* loadForegroundTab() {
+  // Spawn a content task that resolves once we're sure the visibilityState was
+  // changed. This state is what the tests in this file rely on.
+  let promise = ContentTask.spawn(gBrowser.selectedTab.linkedBrowser, null, function* () {
+    return new Promise(resolve => {
+      let document = content.document;
+      document.addEventListener("visibilitychange", function onStateChange() {
+        Assert.equal(document.visibilityState, "hidden", "UITour page should be hidden now.");
+        document.removeEventListener("visibilitychange", onStateChange);
+        resolve();
+      });
+    });
+  });
   yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+  yield promise;
   isnot(gBrowser.selectedTab, gTestTab, "Make sure tour tab isn't selected");
 }
--- a/browser/extensions/loop/bootstrap.js
+++ b/browser/extensions/loop/bootstrap.js
@@ -531,18 +531,29 @@ var WindowListener = {
           // it for each individual tab's browsers.
           gBrowser.addEventListener("mousemove", this);
           gBrowser.addEventListener("click", this);
         }
 
         this._maybeShowBrowserSharingInfoBar();
 
         // Get the first window Id for the listener.
-        this.LoopAPI.broadcastPushMessage("BrowserSwitch",
-          gBrowser.selectedBrowser.outerWindowID);
+        let browser = gBrowser.selectedBrowser;
+        return new Promise(resolve => {
+          if (browser.outerWindowID) {
+            resolve(browser.outerWindowID);
+            return;
+          }
+
+          browser.messageManager.addMessageListener("Browser:Init", function initListener() {
+            browser.messageManager.removeMessageListener("Browser:Init", initListener);
+            resolve(browser.outerWindowID);
+          });
+        }).then(outerWindowID =>
+          this.LoopAPI.broadcastPushMessage("BrowserSwitch", outerWindowID));
       },
 
       /**
        * Stop listening to selected tab changes.
        */
       stopBrowserSharing: function() {
         if (!this._listeningToTabSelect) {
           return;
--- a/browser/extensions/loop/chrome/content/panels/css/desktop.css
+++ b/browser/extensions/loop/chrome/content/panels/css/desktop.css
@@ -25,17 +25,16 @@
 .room-invitation-content {
   display: flex;
   flex-flow: column nowrap;
   margin: 12px 0;
   font-size: 1.4rem;
 }
 
 .room-invitation-content > * {
-  width: 100%;
   margin: 0 15px;
 }
 
 .room-context-header {
   font-weight: bold;
   font-size: 1.6rem;
   margin-bottom: 10px;
   text-align: center;
@@ -225,20 +224,16 @@ html[dir="rtl"] .share-panel-container >
   right: initial;
   transform: translate(-100%);
 }
 
 .share-panel-container > .room-invitation-overlay > .room-invitation-content {
   margin: 0 0 12px;
 }
 
-.share-panel-container > .room-invitation-overlay > .room-invitation-content > * {
-  width: initial;
-}
-
 .share-panel-open > .room-invitation-overlay,
 html[dir="rtl"] .share-panel-open > .room-invitation-overlay {
   transform: translateX(0);
 }
 
 .share-panel-open > .share-panel-overlay {
   display: block;
 }
--- a/browser/extensions/loop/chrome/content/panels/js/panel.js
+++ b/browser/extensions/loop/chrome/content/panels/js/panel.js
@@ -1086,22 +1086,22 @@ loop.panel = function (_, mozL10n) {
         this.setState({
           showPanel: true
         });
       }
     },
 
     handleClosePanel: function () {
       this.props.onSharePanelDisplayChange();
+      this.openRoom();
+      this.closeWindow();
+
       this.setState({
         showPanel: false
       });
-
-      this.openRoom();
-      this.closeWindow();
     },
 
     openRoom: function () {
       var activeRoom = this.state.activeRoom;
       this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
         roomToken: activeRoom.roomToken
       }));
     },
--- a/browser/extensions/loop/chrome/content/panels/js/roomViews.js
+++ b/browser/extensions/loop/chrome/content/panels/js/roomViews.js
@@ -351,16 +351,17 @@ loop.roomViews = function (mozL10n) {
                   localVideoMuted: this.state.videoMuted,
                   matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
                   remotePosterUrl: this.props.remotePosterUrl,
                   remoteSrcMediaElement: this.state.remoteSrcMediaElement,
                   renderRemoteVideo: this.shouldRenderRemoteVideo(),
                   screenShareMediaElement: this.state.screenShareMediaElement,
                   screenSharePosterUrl: null,
                   showInitialContext: false,
+                  showMediaWait: false,
                   showTile: false },
                 React.createElement(sharedViews.ConversationToolbar, {
                   audio: { enabled: !this.state.audioMuted, visible: true },
                   dispatcher: this.props.dispatcher,
                   hangup: this.leaveRoom,
                   showHangup: this.props.chatWindowDetached,
                   video: { enabled: !this.state.videoMuted, visible: true } }),
                 React.createElement(sharedDesktopViews.SharePanelView, {
--- a/browser/extensions/loop/chrome/content/preferences/prefs.js
+++ b/browser/extensions/loop/chrome/content/preferences/prefs.js
@@ -1,12 +1,17 @@
 pref("loop.enabled", true);
 pref("loop.remote.autostart", true);
+#ifdef LOOP_DEV_XPI
+pref("loop.server", "https://loop-dev.stage.mozaws.net/v0");
+pref("loop.linkClicker.url", "https://loop-webapp-dev.stage.mozaws.net/");
+#else
 pref("loop.server", "https://loop.services.mozilla.com/v0");
 pref("loop.linkClicker.url", "https://hello.firefox.com/");
+#endif
 pref("loop.gettingStarted.latestFTUVersion", 1);
 pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start/");
 pref("loop.gettingStarted.resumeOnFirstJoin", false);
 pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");
 pref("loop.do_not_disturb", false);
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
--- a/browser/extensions/loop/chrome/content/shared/css/conversation.css
+++ b/browser/extensions/loop/chrome/content/shared/css/conversation.css
@@ -505,50 +505,84 @@ html, .fx-embedded, #main,
    fix its height. */
 .media-wrapper > .text-chat-view {
   flex: 0 0 auto;
   height: 100%;
   /* Text chat is a fixed 272px width for normal displays. */
   width: 272px;
 }
 
-.media-wrapper.showing-local-streams > .text-chat-view {
+.media-wrapper > .text-chat-view > .text-chat-entries > .text-chat-scroller > .welcome-message {
+  font-size: 1.2rem;
+  margin: 0 0 15px;
+  color: #5e5f64;
+  line-height: 20px;
+}
+
+.media-wrapper.showing-local-streams > .text-chat-view,
+.media-wrapper.showing-media-wait > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
      chat view a bit shorter to give room. */
   height: calc(100% - 204px);
 }
 
+.media-wrapper.showing-media-wait > .text-chat-view {
+  order: 2;
+}
+
+.media-wrapper.showing-media-wait > .local {
+  /* Hides the local stream video box while we're asking the user for permissions */
+  display: none;
+}
+
 .media-wrapper.showing-local-streams.receiving-screen-share {
   position: relative;
 }
 
 .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
-     chat view a bit shorter to give room. 2 streams x 204px each*/
-  height: calc(100% - 408px);
+     chat view a bit shorter to give room. 1 streams x 204px */
+  height: calc(100% - 204px);
 }
 
 .media-wrapper.receiving-screen-share > .screen {
   order: 1;
 }
 
-.media-wrapper.receiving-screen-share > .text-chat-view {
-  order: 2;
+.media-wrapper.receiving-screen-share > .text-chat-view,
+.media-wrapper.showing-local-streams > .text-chat-view  {
+  order: 4;
 }
 
 .media-wrapper.receiving-screen-share > .remote {
   flex: 0 1 auto;
-  order: 3;
+  order: 2;
   /* to keep the 4:3 ratio set both height and width */
   height: 204px;
   width: 272px;
 }
 
 .media-wrapper.receiving-screen-share > .local {
-  order: 4;
+  order: 3;
+}
+
+.media-wrapper.receiving-screen-share.showing-remote-streams > .local {
+  position: absolute;
+  z-index: 2;
+  padding: 8px;
+  right: 0;
+  left: auto;
+  top: 124px;
+  /* to keep the 4:3 ratio 80x60px + 16px padding + 4px border */
+  width: calc(80px + 16px + 4px);
+  height: calc(60px + 16px + 4px);
+}
+
+.media-wrapper.receiving-screen-share.showing-remote-streams > .local > .remote-video-box {
+  border: solid 2px #fff;
 }
 
 @media screen and (max-width:640px) {
   .media-layout > .media-wrapper {
     flex-direction: row;
     margin: 0;
     width: 100%;
   }
@@ -625,34 +659,36 @@ html, .fx-embedded, #main,
     max-width: 50%;
   }
 
   .media-wrapper.receiving-screen-share > .remote .remote-video {
       /* Reset the object-fit for this. */
     object-fit: contain;
   }
 
-  .media-wrapper.receiving-screen-share > .local {
+  .media-wrapper.receiving-screen-share.showing-remote-streams > .local {
     /* Screen shares have remote & local video side-by-side on narrow screens */
     order: 3;
     flex: 1 1 auto;
     height: 20%;
     /* Ensure no previously specified widths take effect, and we take up no more
        than half the width. */
     width: auto;
     max-width: 50%;
     /* This cancels out the absolute positioning when it's just remote video. */
     position: relative;
+    top: auto;
     bottom: auto;
     right: auto;
     margin: 0;
+    padding: 0;
   }
 
-  .media-wrapper.receiving-screen-share > .text-chat-view {
-    order: 4;
+  .media-wrapper.receiving-screen-share.showing-remote-streams > .local > .remote-video-box {
+    border: 0;
   }
 }
 
 /* e.g. very narrow widths similar to conversation window.
    Note: on some displays (e.g. windows / medium size) the width
    may be very slightly over the expected width, so we add on 2px
    just in case. */
 @media screen and (max-width:352px) {
--- a/browser/extensions/loop/chrome/content/shared/js/textChatView.js
+++ b/browser/extensions/loop/chrome/content/shared/js/textChatView.js
@@ -211,16 +211,21 @@ loop.shared.views.chat = function (mozL1
       });
 
       return React.createElement(
         "div",
         { className: entriesClasses },
         React.createElement(
           "div",
           { className: "text-chat-scroller" },
+          loop.shared.utils.isDesktop() ? null : React.createElement(
+            "p",
+            { className: "welcome-message" },
+            mozL10n.get("rooms_welcome_text_chat_label", { clientShortname: mozL10n.get("clientShortname2") })
+          ),
           this.props.messageList.map(function (entry, i) {
             if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
               if (!this.props.showInitialContext) {
                 return null;
               }
               switch (entry.contentType) {
                 case CHAT_CONTENT_TYPES.ROOM_NAME:
                   return React.createElement(TextChatRoomName, {
--- a/browser/extensions/loop/chrome/content/shared/js/views.js
+++ b/browser/extensions/loop/chrome/content/shared/js/views.js
@@ -880,16 +880,17 @@ loop.shared.views = function (_, mozL10n
       matchMedia: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       remoteSrcMediaElement: React.PropTypes.object,
       renderRemoteVideo: React.PropTypes.bool.isRequired,
       screenShareMediaElement: React.PropTypes.object,
       screenSharePosterUrl: React.PropTypes.string,
       screenSharingPaused: React.PropTypes.bool,
       showInitialContext: React.PropTypes.bool.isRequired,
+      showMediaWait: React.PropTypes.bool.isRequired,
       showTile: React.PropTypes.bool.isRequired
     },
 
     isLocalMediaAbsolutelyPositioned: function (matchMedia) {
       if (!matchMedia) {
         matchMedia = this.props.matchMedia;
       }
       return matchMedia && (
@@ -937,32 +938,57 @@ loop.shared.views = function (_, mozL10n
           displayAvatar: this.props.localVideoMuted,
           isLoading: this.props.isLocalLoading,
           mediaType: "local",
           posterUrl: this.props.localPosterUrl,
           srcMediaElement: this.props.localSrcMediaElement })
       );
     },
 
+    renderMediaWait: function () {
+      var msg = mozL10n.get("call_progress_getting_media_description", { clientShortname: mozL10n.get("clientShortname2") });
+      var utils = loop.shared.utils;
+      var isChrome = utils.isChrome(navigator.userAgent);
+      var isFirefox = utils.isFirefox(navigator.userAgent);
+      var isOpera = utils.isOpera(navigator.userAgent);
+      var promptMediaMessageClasses = classNames({
+        "prompt-media-message": true,
+        "chrome": isChrome,
+        "firefox": isFirefox,
+        "opera": isOpera,
+        "other": !isChrome && !isFirefox && !isOpera
+      });
+      return React.createElement(
+        "div",
+        { className: "prompt-media-message-wrapper" },
+        React.createElement(
+          "p",
+          { className: promptMediaMessageClasses },
+          msg
+        )
+      );
+    },
+
     render: function () {
       var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
       var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare,
         "screen-sharing-paused": this.props.screenSharingPaused
       });
 
       var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement || this.props.localPosterUrl,
+        "showing-media-wait": this.props.showMediaWait,
         "showing-remote-streams": this.props.remoteSrcMediaElement || this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
       return React.createElement(
         "div",
         { className: "media-layout" },
         React.createElement(
           "div",
@@ -998,17 +1024,18 @@ loop.shared.views = function (_, mozL10n
               shareCursor: true,
               srcMediaElement: this.props.screenShareMediaElement }),
             this.props.displayScreenShare ? this.props.children : null
           ),
           React.createElement(loop.shared.views.chat.TextChatView, {
             dispatcher: this.props.dispatcher,
             showInitialContext: this.props.showInitialContext,
             showTile: this.props.showTile }),
-          this.state.localMediaAboslutelyPositioned ? null : this.renderLocalVideo()
+          this.state.localMediaAboslutelyPositioned ? null : this.renderLocalVideo(),
+          this.props.showMediaWait ? this.renderMediaWait() : null
         )
       );
     }
   });
 
   var RemoteCursorView = React.createClass({
     displayName: "RemoteCursorView",
 
--- a/browser/extensions/loop/chrome/content/shared/test/views_test.js
+++ b/browser/extensions/loop/chrome/content/shared/test/views_test.js
@@ -826,16 +826,17 @@ describe("loop.shared.views", function()
         displayScreenShare: false,
         isLocalLoading: false,
         isRemoteLoading: false,
         isScreenShareLoading: false,
         localVideoMuted: false,
         matchMedia: window.matchMedia,
         renderRemoteVideo: false,
         showInitialContext: false,
+        showMediaWait: false,
         showTile: false
       };
 
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.MediaLayoutView,
           _.extend(defaultProps, extraProps)));
     }
 
@@ -963,16 +964,34 @@ describe("loop.shared.views", function()
       view = mountTestComponent({
         remoteSrcMediaElement: {},
         remotePosterUrl: "fake/url"
       });
 
       expect(view.getDOMNode().querySelector(".media-wrapper")
         .classList.contains("showing-remote-streams")).eql(true);
     });
+
+    it("should mark the wrapper as showing media wait tile when asking for user media", function() {
+      view = mountTestComponent({
+        showMediaWait: true
+      });
+
+      expect(view.getDOMNode().querySelector(".media-wrapper")
+        .classList.contains("showing-media-wait")).eql(true);
+    });
+
+    it("should display a media wait tile when asking for user media", function() {
+      view = mountTestComponent({
+        showMediaWait: true
+      });
+
+      expect(view.getDOMNode().querySelector(".prompt-media-message-wrapper"))
+        .not.eql(null);
+    });
   });
 
   describe("RemoteCursorView", function() {
     var view;
     var fakeVideoElementSize;
     var remoteCursorStore;
 
     function mountTestComponent(props) {
--- a/browser/extensions/loop/chrome/locale/fa/loop.properties
+++ b/browser/extensions/loop/chrome/locale/fa/loop.properties
@@ -1,16 +1,14 @@
 # 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/.
 
 # Panel Strings
 
-
-
 ## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
 ## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
 ## use "..." if \u2026 doesn't suit traditions in your locale.
 loopMenuItem_label=شروع یک گفت‌وگو…
 loopMenuItem_accesskey=t
 
 ## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
 ## These are displayed together at the top of the panel when a user is needed to
@@ -39,32 +37,32 @@ first_time_experience_content=برای برنامه‌ریزی گروهی، کار کردن گروهی و خندیدن با هم استفاده کنید.
 first_time_experience_content2=برای به سرانجام رساندن کارها از آن استفاده کنید: با هم برنامه‌ریزی کنید، با هم بخندید، با هم کار کنید.
 first_time_experience_button_label2=نحوه کار را یاد بگیرید
 
 ## First Time Experience Slides
 fte_slide_1_title=صفحات وب را با یک دوست مرور کنید
 ## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
 ## will be replaced by the short name 2.
 fte_slide_1_copy=اگر در حال برنامه‌ریزی برای یک سفر هستید یا قصد خرید یک هدیه را دارید، {{clientShortname2}} به شما اجازه می‌دهد تا زمانی کوتاه تصمیمات سریع‌تر بگیرید.
-fte_slide_2_title=با هم هماهنگ باشید
-fte_slide_2_copy=از سیستم گپ ویدئویی یا متنی برای به اشتراک گذاشتن ایده‌ها، مقایسه گزینه‌ها و رسیدن به تفاهم، استفاده کنید.
+fte_slide_2_title2=ساخته شده برای اشتراک گذاری وب
+## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
+## will be replaced by the short name 2.
+fte_slide_2_copy2=حالا اگر یک دوست خود را به یک نشست دعوت کنید، {{clientShortname2}} بطور خودکار هر صفحه وبی که دیدن می‌کنید را به اشتراک می‌گذارد. باهم. برنامه‌ریزی کنید. خرید کنید. تصمیم بگیرد.
 fte_slide_3_title=یک دوست را با ارسال یک پیوند دعوت کنید
 ## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 fte_slide_3_copy={{clientSuperShortname}} با اکثر مرورگرهای رومیزی کار می‌کند. هیچ حسابی لازم نیست و هر کسی می‌تواند به رایگان متصل شود.
 ## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 fte_slide_4_title=شمایل {{clientSuperShortname}} را برای شروع پیدا کنید
 ## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
 ## will be replaced by the brand short name.
 fte_slide_4_copy=زمانی که صفحه‌ای پیدا کردید تا موردش بحث کنید، بر روی شمایل {{brandShortname}} کلیک کنید تا یک پیوند بسازید. سپس آن را هر طور که دوست دارید برای دوست خود ارسال کنید!
 
-invite_header_text_bold=یک نفر را برای همراهی در مرور اینه صفحه دعوت کنید!
 invite_header_text_bold2=از یک دوست برای پیوستن به شما دعوت کنید!
-invite_header_text3=برای کار کردن با Firefox Hello باید دو نفر باشید، پس برای دوست خود یک پیوند بفرستید تا وب را با هم مرور کنید!
 invite_header_text4=این پیوند را برای شروع مرور وب با هم به اشتراک بگذارید.
 ## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
 ## invite_email_link_button, invite_facebook_button2): These labels appear under
 ## an iconic button for the invite view.
 invite_copy_link_button=رونوشت از پیوند
 invite_copied_link_button=رونوشت شد!
 invite_email_link_button=پست پیوند
 invite_facebook_button3=فیس‌بوک
@@ -102,16 +100,17 @@ share_email_footer2=\n\n____________\n Firefox Hello به شما اجازه می‌دهد تا وب را با دوستان خود مرور کنید. از آن وقتی که می‌خواهید کارها به سرانجام برسد استفاده کنید: باهم برنامه‌ریزی کنید، باهم بخندید. در http://www.firefox.com/hello بیشتر مطلع شوید
 share_tweet=به من در یک گفت‌وگو ویدئویی در {{clientShortname2}} بپیوندید!
 
 share_add_service_button=اضافه کردن یک سرویس
 
 ## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
 ## These menu items are displayed from a panel's context menu for a conversation.
 copy_link_menuitem=رونوشت پیوند
 email_link_menuitem=پست کردن پیوند
+edit_name_menuitem=ویرایش نام
 delete_conversation_menuitem2=حذف
 
 panel_footer_signin_or_signup_link=ورود یا ثبت‌نام
 
 settings_menu_item_account=حساب
 settings_menu_item_settings=تنظیمات
 settings_menu_item_signout=خروج
 settings_menu_item_signin=ورود
--- a/browser/extensions/loop/chrome/locale/fr/loop.properties
+++ b/browser/extensions/loop/chrome/locale/fr/loop.properties
@@ -29,17 +29,17 @@ panel_disconnect_button=Déconnexion
 ## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
 ## user to create his or her first conversation.
 first_time_experience_subheading2=Cliquez sur le bouton Hello pour consulter des pages web avec une autre personne.
 first_time_experience_subheading_button_above=Cliquez sur le bouton ci-dessus pour naviguer sur le Web avec une autre personne.
 
 ## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
 ## ways to use Hello project.
 first_time_experience_content=Utilisez-le pour vous organiser, travailler et rire ensemble.
-first_time_experience_content2=Utilisez vous pour réaliser vos projets : vous organiser, travailler et rire ensemble.
+first_time_experience_content2=Utilisez-le pour réaliser vos projets : vous organiser, travailler et rire ensemble.
 first_time_experience_button_label2=Principe de fonctionnement
 
 ## First Time Experience Slides
 fte_slide_1_title=Naviguez sur le Web avec une autre personne
 ## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
 ## will be replaced by the short name 2.
 fte_slide_1_copy=Que ce soit pour planifier un voyage ou l’achat d’un cadeau, {{clientShortname2}} vous permet de prendre des décisions plus rapidement.
 fte_slide_2_title2=Conçu pour partager le Web
@@ -92,17 +92,17 @@ share_email_body7=Une autre personne vous attend sur Firefox Hello. Cliquez sur le lien pour vous connecter et naviguer sur une page web ensemble : {{callUrl}}
 ## LOCALIZATION NOTE (share_email_body_context3): In this item, don't translate
 ## the part between {{..}} and leave the \n\n part alone.
 share_email_body_context3=Une autre personne vous attend sur Firefox Hello. Cliquez sur le lien pour vous connecter et naviguer sur {{title}} ensemble : {{callUrl}}
 ## LOCALIZATION NOTE (share_email_footer2): Common footer content for both email types
 share_email_footer2=\n\n____________\nFirefox Hello vous permet de naviguer sur le Web avec vos amis. Vous pouvez faire des tas de choses : planifier, travailler ou rire ensemble. Apprenez-en davantage sur http://www.firefox.com/hello
 ## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
 ## between {{..}}. Please keep the text below 117 characters to make sure it fits
 ## in a tweet.
-share_tweet=Rejoignez-moi pour une conversation vidéo sur {{clientShortname2}} !
+share_tweet=Rejoignez-moi pour une conversation vidéo sur {{clientShortname2}} !
 
 share_add_service_button=Ajouter un service
 
 ## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
 ## These menu items are displayed from a panel's context menu for a conversation.
 copy_link_menuitem=Copier le lien
 email_link_menuitem=Envoyer le lien
 edit_name_menuitem=Modifier le nom
--- a/browser/extensions/loop/chrome/locale/id/loop.properties
+++ b/browser/extensions/loop/chrome/locale/id/loop.properties
@@ -1,16 +1,14 @@
 # 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/.
 
 # Panel Strings
 
-
-
 ## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
 ## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
 ## use "..." if \u2026 doesn't suit traditions in your locale.
 loopMenuItem_label=Mulai sebuah percakapan …
 loopMenuItem_accesskey=t
 
 ## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
 ## These are displayed together at the top of the panel when a user is needed to
@@ -39,32 +37,32 @@ first_time_experience_content=Gunakan in
 first_time_experience_content2=Pergunakanlah untuk menyelesaikan sesuatu: merencanakan bersama, tertawa bersama, bekerja bersama.
 first_time_experience_button_label2=Lihat cara kerja
 
 ## First Time Experience Slides
 fte_slide_1_title=Menjelajahi laman Web bersama teman
 ## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
 ## will be replaced by the short name 2.
 fte_slide_1_copy=Baik merencanakan perjalanan atau berbelanja hadiah, {{clientShortname2}} memungkinkan anda membuat keputusan lebih cepat dalam waktu singkat.
-fte_slide_2_title=Memiliki pemahaman yang sama
-fte_slide_2_copy=Menggunakan teks atau percakapan video yang tersedia untuk berbagi gagasan, emmbandingkan pilihan dan bermufakat.
+fte_slide_2_title2=Dirancang untuk berbagi Web
+## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
+## will be replaced by the short name 2.
+fte_slide_2_copy2=Sekarang saat Anda mengundang teman ke sebuah sesi, {{clientShortname2}} akan otomatis bagikan laman Web yang sedang Anda lihat. Rencanakan. Belanja. Tentukan. Bersama.
 fte_slide_3_title=Mengundang teman dengan mengirimkan tautan
 ## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 fte_slide_3_copy={{clientSuperShortname}} bekerja dengan hampir semua peramban. Tidak perlu membuat akun dan setiap orang terhubung secara gratis.
 ## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 fte_slide_4_title=Temukan ikon {{clientSuperShortname}} untuk memulai
 ## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
 ## will be replaced by the brand short name.
 fte_slide_4_copy=Saat Anda menemukan laman yang ingin didiskusikan, klik ikon {{brandShortname}} untuk membuat tautan. Lalu kirimkan ke teman seperti yang Anda inginkan!
 
-invite_header_text_bold=Undang seseorang untuk jelajahi laman ini bersama Anda!
 invite_header_text_bold2=Mengundang teman untuk bergabung!
-invite_header_text3=Membutuhkan 2 orang untuk menggunakan Firefox Hello, jadi kirimkan tautan ke teman untuk menjelajah Web bersama Anda!
 invite_header_text4=Bagikan tautan ini supaya anda bisa mulai menjelajahi Web bersama.
 ## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
 ## invite_email_link_button, invite_facebook_button2): These labels appear under
 ## an iconic button for the invite view.
 invite_copy_link_button=Salin Tautan
 invite_copied_link_button=Tersalin!
 invite_email_link_button=Kirim Tautan
 invite_facebook_button3=Facebook
@@ -102,16 +100,17 @@ share_email_footer2=\n\n____________\nFi
 share_tweet=Gabung dengan saya untuk percakapan video pada {{clientShortname2}}!
 
 share_add_service_button=Tambah Layanan
 
 ## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
 ## These menu items are displayed from a panel's context menu for a conversation.
 copy_link_menuitem=Salin Tautan
 email_link_menuitem=Kirim Tautan
+edit_name_menuitem=Ubah nama
 delete_conversation_menuitem2=Hapus
 
 panel_footer_signin_or_signup_link=Masuk atau Daftar
 
 settings_menu_item_account=Akun
 settings_menu_item_settings=Pengaturan
 settings_menu_item_signout=Keluar
 settings_menu_item_signin=Masuk
--- a/browser/extensions/loop/chrome/locale/kk/loop.properties
+++ b/browser/extensions/loop/chrome/locale/kk/loop.properties
@@ -1,16 +1,14 @@
 # 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/.
 
 # Panel Strings
 
-
-
 ## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
 ## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
 ## use "..." if \u2026 doesn't suit traditions in your locale.
 loopMenuItem_label=Сөйлесуді бастау…
 loopMenuItem_accesskey=т
 
 ## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
 ## These are displayed together at the top of the panel when a user is needed to
@@ -38,27 +36,26 @@ first_time_experience_subheading_button_above=Веб парақтарды достармен бірге қарау үшін жоғарыдағы батырманы басыңыз.
 first_time_experience_content=Оны достармен бірге жоспарлау, жұмыс жасау және бірге күлу үшін қолданыңыз.
 first_time_experience_content2=Оны істерді бітіру үшін қолданыңыз: достармен бірге жоспарлау, жұмыс жасау және бірге күлу.
 first_time_experience_button_label2=Бұл қалай жұмыс жасайтынын қарау
 
 ## First Time Experience Slides
 fte_slide_1_title=Веб парақтарды досыңызбен бірге шолу
 ## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
 ## will be replaced by the short name 2.
-fte_slide_2_title=Бір парақты ашыңыз
+## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
+## will be replaced by the short name 2.
 fte_slide_3_title=Досыңызды сөйлесуге оған сілтемені жіберу арқылы шақырыңыз
 ## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 ## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
 ## will be replaced by the super short brand name.
 ## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
 ## will be replaced by the brand short name.
 
-invite_header_text_bold=Біреуді бұл парақты сізбен бірге шолуға шақырыңыз!
-invite_header_text3=Firefox Hello қолдану үшін екі адам керек, сондықтан, досыңызға интернетті бірге шолуға сілтемені жіберіңіз!
 ## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
 ## invite_email_link_button, invite_facebook_button2): These labels appear under
 ## an iconic button for the invite view.
 invite_copy_link_button=Сілтемені көшіріп алу
 invite_copied_link_button=Көшірілген!
 invite_email_link_button=Сілтемені эл. поштамен жіберу
 invite_facebook_button3=Facebook
 invite_your_link=Сіздің сілтемеңіз:
@@ -94,16 +91,17 @@ share_email_body_context3=Досыңыз сізді Firefox Hello-да күтіп тұр. Байланысты орнату және {{title}} бірге шолу үшін сілтемені шертіңіз: {{callUrl}}
 share_tweet=Менімен {{clientShortname2}} видео сөйлесуіне қатысыңыз!
 
 share_add_service_button=Қызметті қосу
 
 ## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
 ## These menu items are displayed from a panel's context menu for a conversation.
 copy_link_menuitem=Сілтемені көшіріп алу
 email_link_menuitem=Сілтемені эл. поштамен жіберу
+edit_name_menuitem=Атын түзету
 delete_conversation_menuitem2=Өшіру
 
 panel_footer_signin_or_signup_link=Кіру немесе тіркелгіні жасау
 
 settings_menu_item_account=Тіркелгі
 settings_menu_item_settings=Баптаулар
 settings_menu_item_signout=Шығу
 settings_menu_item_signin=Кіру
@@ -241,16 +239,19 @@ rooms_room_full_call_to_action_label={{clientShortname}} туралы көбірек біліңіз »
 rooms_room_full_call_to_action_nonFx_label=Өз сөйлесуіңізді бастау үшін {{brandShortname}} жүктеп алыңыз
 rooms_room_full_label=Бұл сөйлесуге екі адам қатысуда.
 rooms_room_join_label=Сөйлесуге қосылу
 rooms_room_joined_owner_connected_label2=Сіздің досыңыз енді байланысқан, және сіздің браузер беттерін көре алады.
 rooms_room_joined_owner_not_connected_label=Сіздің досыңыз {{roomURLHostname}} сайтын бірге қарауға сізді күтіп тұр.
 
 self_view_hidden_message=Өздік көрініс жасырылған, бірақ, жіберілуде; көрсету үшін терезе өлшемін өзгертіңіз
 
+peer_left_session=Досыңыз шықты.
+peer_unexpected_quit=Досыңыз күтпегенде үзілген.
+
 ## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
 ## as this will be replaced by clientShortname2.
 tos_failure_message={{clientShortname}} сіздің еліңізде қолжетерсіз.
 
 display_name_guest=Қонақ
 
 ## LOCALIZATION NOTE(clientSuperShortname): This should not be localized and
 ## should remain "Hello" for all locales.
--- a/browser/extensions/loop/chrome/locale/rm/loop.properties
+++ b/browser/extensions/loop/chrome/locale/rm/loop.properties
@@ -1,16 +1,14 @@
 # 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/.
 
 # Panel Strings
 
-
-
 ## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
 ## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
 ## use "..." if \u2026 doesn't suit traditions in your locale.
 loopMenuItem_label=Cumenzar ina conversaziun…
 loopMenuItem_accesskey=t
 
 ## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
 ## These are displayed together at the top of the panel when a user is needed to
@@ -23,39 +21,46 @@ sign_in_again_title_line_two2=per cuntin
 sign_in_again_button=S'annunziar
 ## LOCALIZATION_NOTE(sign_in_again_use_as_guest_button2): {{clientSuperShortname}}
 ## will be replaced by the super short brandname.
 sign_in_again_use_as_guest_button2=Utilisar {{clientSuperShortname}} sco giast
 
 panel_browse_with_friend_button=Navigar cun in ami en questa pagina
 panel_disconnect_button=Deconnectar
 
-## LOCALIZATION_NOTE(first_time_experience_subheading2): Message inviting the
+## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
 ## user to create his or her first conversation.
 first_time_experience_subheading2=Clicca sin il buttun da Hello per navigar cun in ami en il web.
 
-## LOCALIZATION_NOTE(first_time_experience_content): Message describing
+## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
 ## ways to use Hello project.
 first_time_experience_content=Fa diever da la funcziun per planisar ensemen, lavurar ensemen, rir ensemen.
 first_time_experience_button_label2=Mussar co che quai funcziunescha
 
-invite_header_text_bold=Envida insatgi a navigar cun tai sin questa pagina!
-invite_header_text3=I dovra dus per utilisar Firefox Hello. Trametta pia ina colliaziun ad in ami per ch'el possia navigar en il web cun tai!
+## First Time Experience Slides
+## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
+## will be replaced by the short name 2.
+## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
+## will be replaced by the short name 2.
+## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
+## will be replaced by the super short brand name.
+## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
+## will be replaced by the super short brand name.
+## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
+## will be replaced by the brand short name.
+
 ## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
 ## invite_email_link_button, invite_facebook_button2): These labels appear under
 ## an iconic button for the invite view.
 invite_copy_link_button=Copiar la colliaziun
 invite_copied_link_button=Copià!
 invite_email_link_button=Trametter la colliaziun via e-mail
 invite_facebook_button3=Facebook
 invite_your_link=Tia colliaziun:
 
-# Status text
-display_name_guest=Giast
-
 # Error bars
 ## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
 ## These may be displayed at the top of the panel.
 session_expired_error_description=La sessiun è scrudada. Tut las URLs che ti has creà e cundividì fin ussa na vegnan betg pli a funcziunar.
 could_not_authenticate=Impussibel dad autentifitgar
 password_changed_question=Has ti midà tes pled-clav?
 try_again_later=Emprova p.pl. pli tard anc ina giada
 could_not_connect=Impussibel da connectar cun il server
--- a/browser/extensions/loop/chrome/test/mochitest/browser.ini
+++ b/browser/extensions/loop/chrome/test/mochitest/browser.ini
@@ -9,11 +9,10 @@ support-files =
 [browser_loop_fxa_server.js]
 [browser_LoopRooms_channel.js]
 [browser_menuitem.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_chat.js]
 [browser_mozLoop_context.js]
 [browser_mozLoop_socialShare.js]
 [browser_mozLoop_sharingListeners.js]
-skip-if = e10s
 [browser_mozLoop_telemetry.js]
 [browser_toolbarbutton.js]
--- a/browser/extensions/loop/chrome/test/mochitest/browser_mozLoop_sharingListeners.js
+++ b/browser/extensions/loop/chrome/test/mochitest/browser_mozLoop_sharingListeners.js
@@ -46,20 +46,26 @@ function* promiseWindowIdReceivedNewTab(
   for (let handler of handlersParam) {
     handler.windowId = windowId;
   }
 }
 
 function promiseNewTabLocation() {
   BrowserOpenTab();
   let tab = gBrowser.selectedTab;
+  let browser = tab.linkedBrowser;
   createdTabs.push(tab);
 
-  // Have the tab's content process pass back its location as a promise
-  return ContentTask.spawn(tab.linkedBrowser, null, () => content.location.href);
+  // If we're already loaded, then just get the location.
+  if (browser.contentDocument.readyState === "complete") {
+    return ContentTask.spawn(browser, null, () => content.location.href);
+  }
+
+  // Otherwise, wait for the load to complete.
+  return BrowserTestUtils.browserLoaded(browser);
 }
 
 function promiseRemoveTab(tab) {
   return new Promise(resolve => {
     gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
       gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
       resolve();
     });
@@ -195,16 +201,17 @@ add_task(function* test_infoBar() {
 add_task(function* test_newtabLocation() {
   // Check location before sharing
   let locationBeforeSharing = yield promiseNewTabLocation();
   Assert.equal(locationBeforeSharing, "about:newtab");
 
   // Check location after sharing
   yield promiseWindowIdReceivedOnAdd(handlers[0]);
   let locationAfterSharing = yield promiseNewTabLocation();
+  info("Location after sharing: " + locationAfterSharing);
   Assert.ok(locationAfterSharing.match(/about:?home/));
 
   // Check location after stopping sharing
   gHandlers.RemoveBrowserSharingListener({ data: [listenerIds.pop()] }, function() {});
   let locationAfterStopping = yield promiseNewTabLocation();
   Assert.equal(locationAfterStopping, "about:newtab");
 
   yield removeTabs();
--- a/browser/extensions/loop/install.rdf.in
+++ b/browser/extensions/loop/install.rdf.in
@@ -4,17 +4,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>loop@mozilla.org</em:id>
     <em:bootstrap>true</em:bootstrap>
-    <em:version>1.2.2</em:version>
+    <em:version>1.2.4</em:version>
     <em:type>2</em:type>
 
     <!-- Target Application this extension can install into,
          with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>46.0a1</em:minVersion>
--- a/browser/extensions/loop/jar.mn
+++ b/browser/extensions/loop/jar.mn
@@ -1,17 +1,17 @@
 #filter substitution
 # 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/.
 
 [features/loop@mozilla.org] chrome.jar:
 % content loop %content/ contentaccessible=yes
 % content loop-locale-fallback %content/locale-fallback/en-US/
-% skin loop classic/1.0 %skin/linux/ os=Linux
+% skin loop classic/1.0 %skin/linux/
 % skin loop classic/1.0 %skin/osx/ os=Darwin
 % skin loop classic/1.0 %skin/windows/ os=WINNT
 % skin loop-shared classic/1.0 %skin/shared/
 % override chrome://loop/skin/menuPanel.png       chrome://loop/skin/menuPanel-yosemite.png       os=Darwin osversion>=10.10
 % override chrome://loop/skin/menuPanel@2x.png    chrome://loop/skin/menuPanel-yosemite@2x.png    os=Darwin osversion>=10.10
 % override chrome://loop/skin/toolbar.png         chrome://loop/skin/toolbar-yosemite.png         os=Darwin osversion>=10.10
 % override chrome://loop/skin/toolbar@2x.png      chrome://loop/skin/toolbar-yosemite@2x.png      os=Darwin osversion>=10.10
 # Windows 10+ uses the default toolbar.png
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -164,17 +164,17 @@ These should match what Safari and other
 <!ENTITY shareVideo.label "Share This Video">
 <!ENTITY shareVideo.accesskey "r">
 <!ENTITY feedsMenu2.label "Subscribe to This Page">
 <!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
 <!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">
 <!ENTITY addCurPagesCmd.label "Bookmark All Tabs…">
 <!ENTITY showAllBookmarks2.label "Show All Bookmarks">
 <!ENTITY recentBookmarks.label "Recently Bookmarked">
-<!ENTITY unsortedBookmarksCmd.label "Unsorted Bookmarks">
+<!ENTITY otherBookmarksCmd.label "Other Bookmarks">
 <!ENTITY bookmarksToolbarChevron.tooltip "Show more bookmarks">
 
 <!ENTITY backCmd.label                "Back">
 <!ENTITY backButton.tooltip           "Go back one page">
 <!ENTITY forwardCmd.label             "Forward">
 <!ENTITY forwardButton.tooltip        "Go forward one page">
 <!ENTITY backForwardButtonMenu.tooltip "Right-click or pull down to show history">
 <!ENTITY backForwardButtonMenuMac.tooltip "Pull down to show history">
@@ -219,17 +219,17 @@ These should match what Safari and other
 
 <!-- Toolbar items --> 
 <!ENTITY homeButton.label             "Home">
 
 <!ENTITY bookmarksButton.label          "Bookmarks">
 <!ENTITY bookmarksCmd.commandkey "b">
 
 <!ENTITY bookmarksMenuButton.label          "Bookmarks">
-<!ENTITY bookmarksMenuButton.unsorted.label "Unsorted Bookmarks">
+<!ENTITY bookmarksMenuButton.other.label "Other Bookmarks">
 <!ENTITY viewBookmarksSidebar2.label        "View Bookmarks Sidebar">
 <!ENTITY viewBookmarksToolbar.label         "View Bookmarks Toolbar">
 
 <!-- LOCALIZATION NOTE (bookmarksSidebarGtkCmd.commandkey): This command
   -  key should not contain the letters A-F, since these are reserved
   -  shortcut keys on Linux. -->
 <!ENTITY bookmarksGtkCmd.commandkey "o">
 <!ENTITY bookmarksWinCmd.commandkey "i">
@@ -249,90 +249,19 @@ These should match what Safari and other
 <!ENTITY downloadsUnix.commandkey     "y">
 <!ENTITY addons.label                 "Add-ons">
 <!ENTITY addons.accesskey             "A">
 <!ENTITY addons.commandkey            "A">
 
 <!ENTITY webDeveloperMenu.label       "Web Developer">
 <!ENTITY webDeveloperMenu.accesskey   "W">
 
-<!ENTITY devToolsCmd.keycode          "VK_F12">
-<!ENTITY devToolsCmd.keytext          "F12">
-
-<!ENTITY devtoolsServiceWorkers.label      "Service Workers">
-<!ENTITY devtoolsServiceWorkers.accesskey  "k">
-
-<!ENTITY devtoolsConnect.label        "Connect…">
-<!ENTITY devtoolsConnect.accesskey    "e">
-
-<!ENTITY errorConsoleCmd.label        "Error Console">
-<!ENTITY errorConsoleCmd.accesskey    "C">
-
-<!ENTITY remoteWebConsoleCmd.label    "Remote Web Console">
-
-<!ENTITY browserConsoleCmd.label      "Browser Console">
-<!ENTITY browserConsoleCmd.commandkey "j">
-<!ENTITY browserConsoleCmd.accesskey  "B">
-
 <!ENTITY inspectContextMenu.label     "Inspect Element">
 <!ENTITY inspectContextMenu.accesskey "Q">
 
-<!ENTITY responsiveDesignMode.label   "Responsive Design Mode">
-<!ENTITY responsiveDesignMode.accesskey "R">
-<!ENTITY responsiveDesignMode.commandkey "M">
-
-<!ENTITY eyedropper.label   "Eyedropper">
-<!ENTITY eyedropper.accesskey "Y">
-
-<!-- LOCALIZATION NOTE (scratchpad.label): This menu item label appears
-  -  in the Tools menu. See bug 653093.
-  -  The Scratchpad is intended to provide a simple text editor for creating
-  -  and evaluating bits of JavaScript code for the purposes of function
-  -  prototyping, experimentation and convenient scripting.
-  -
-  -  It's quite possible that you won't have a good analogue for the word
-  -  "Scratchpad" in your locale. You should feel free to find a close
-  -  approximation to it or choose a word (or words) that means
-  -  "simple discardable text editor". -->
-<!ENTITY scratchpad.label             "Scratchpad">
-<!ENTITY scratchpad.accesskey         "s">
-<!ENTITY scratchpad.keycode           "VK_F4">
-<!ENTITY scratchpad.keytext           "F4">
-
-<!-- LOCALIZATION NOTE (browserToolboxMenu.label): This is the label for the
-  -  application menu item that opens the browser toolbox UI in the Tools menu. -->
-<!ENTITY browserToolboxMenu.label     "Browser Toolbox">
-<!ENTITY browserToolboxMenu.accesskey "e">
-<!ENTITY browserToolboxCmd.commandkey "i">
-
-<!-- LOCALIZATION NOTE (browserContentToolboxMenu.label): This is the label for the
-  -  application menu item that opens the browser content toolbox UI in the Tools menu.
-  -  This toolbox allows to debug the chrome of the content process in multiprocess builds.  -->
-<!ENTITY browserContentToolboxMenu.label     "Browser Content Toolbox">
-<!ENTITY browserContentToolboxMenu.accesskey "x">
-
-<!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
-<!ENTITY devToolbarMenu.label              "Developer Toolbar">
-<!ENTITY devToolbarMenu.accesskey          "v">
-<!ENTITY webide.label                      "WebIDE">
-<!ENTITY webide.accesskey                  "W">
-<!ENTITY webide.keycode                    "VK_F8">
-<!ENTITY webide.keytext                    "F8">
-<!ENTITY devToolbar.keycode                "VK_F2">
-<!ENTITY devToolbar.keytext                "F2">
-<!ENTITY devToolboxMenuItem.label          "Toggle Tools">
-<!ENTITY devToolboxMenuItem.accesskey      "T">
-<!ENTITY devToolboxMenuItem.keytext        "I">
-
-<!ENTITY devToolbarToolsButton.tooltip     "Toggle developer tools">
-<!ENTITY devToolbarOtherToolsButton.label  "More Tools">
-
-<!ENTITY getMoreDevtoolsCmd.label        "Get More Tools">
-<!ENTITY getMoreDevtoolsCmd.accesskey    "M">
-
 <!ENTITY fileMenu.label         "File"> 
 <!ENTITY fileMenu.accesskey       "F">
 <!ENTITY newUserContext.label             "New Container Tab">
 <!ENTITY newUserContext.accesskey         "C">
 <!ENTITY userContextPersonal.label        "Personal">
 <!ENTITY userContextPersonal.accesskey    "P">
 <!ENTITY userContextWork.label            "Work">
 <!ENTITY userContextWork.accesskey        "W">
--- a/build/docs/index.rst
+++ b/build/docs/index.rst
@@ -20,16 +20,17 @@ Important Concepts
    build-targets
    python
    test_manifests
    mozinfo
    preprocessor
    jar-manifests
    defining-binaries
    toolchains
+   locales
 
 integrated development environment (IDE)
 ========================================
 .. toctree::
    :maxdepth: 1
 
    androideclipse
    cppeclipse
new file mode 100644
--- /dev/null
+++ b/build/docs/locales.rst
@@ -0,0 +1,100 @@
+.. _localization:
+
+===================
+Localization (l10n)
+===================
+
+Single-locale language repacks
+==============================
+
+To save on build time, the build system and automation collaborate to allow
+downloading a packaged en-US Firefox, performing some locale-specific
+post-processing, and re-packaging a locale-specific Firefox.  Such artifacts
+are termed "single-locale language repacks".  There is another concept of a
+"multi-locale language build", which is more like a regular build and less
+like a re-packaging post-processing step.
+
+There are scripts in-tree in mozharness to orchestrate these re-packaging
+steps for `Desktop
+<https://dxr.mozilla.org/mozilla-central/source/testing/mozharness/scripts/desktop_l10n.py>`_
+and `Android
+<https://dxr.mozilla.org/mozilla-central/source/testing/mozharness/scripts/mobile_l10n.py>`_
+but they rely heavily on buildbot information so they are almost impossible to
+run locally.
+
+The following instructions are extracted from the `Android script with hg hash
+494289c7
+<https://dxr.mozilla.org/mozilla-central/rev/494289c72ba3997183e7b5beaca3e0447ecaf96d/testing/mozharness/scripts/mobile_l10n.py>`_,
+and may need to be updated and slightly modified for Desktop.
+
+Step by step instructions for Android
+-------------------------------------
+
+This assumes that ``$AB_CD`` is the locale you want to repack with; I tested
+with "ar" and "en-GB".
+
+.. warning:: l10n repacks do not work with artifact builds.  Repackaging
+   compiles no code so supporting ``--disable-compile-environment`` would not
+   save much, if any, time.
+
+#. You must have a built and packaged object directory, or a pre-built
+   ``en-US`` package.
+
+   .. code-block:: shell
+
+      ./mach build
+      ./mach package
+
+#. Clone ``l10n-central/$AB_CD`` so that it is a sibling to your
+   ``mozilla-central`` directory.
+
+   .. code-block:: shell
+
+      $ ls -al
+      mozilla-central
+      ...
+      $ mkdir -p l10n-central
+      $ hg clone https://hg.mozilla.org/l10n-central/$AB_CD l10n-central/$AB_CD
+      $ ls -al
+      mozilla-central
+      l10n-central/$AB_CD
+      ...
+
+#. Copy your ``mozconfig`` to ``mozconfig.l10n`` and add the following.
+
+   ::
+
+      ac_add_options --with-l10n-base=../../l10n-central
+      ac_add_options --disable-tests
+      mk_add_options MOZ_OBJDIR=./objdir-l10n
+
+#. Configure and prepare the l10n object directory.
+
+   .. code-block:: shell
+
+      MOZCONFIG=mozconfig.l10n ./mach configure
+      MOZCONFIG=mozconfig.l10n ./mach build -C config export
+      MOZCONFIG=mozconfig.l10n ./mach build buildid.h
+
+#. Copy your built package and unpack it into the l10n object directory.
+
+   .. code-block:: shell
+
+      cp $OBJDIR/dist/fennec-*en-US*.apk ./objdir-l10n/dist
+      MOZCONFIG=mozconfig.l10n ./mach build -C mobile/android/locales unpack
+
+#. Run the ``compare-locales`` script to write locale-specific changes into
+   ``objdir-l10n/merged``.
+
+   .. code-block:: shell
+
+      MOZCONFIG=mozconfig.l10n ./mach compare-locales --merge-dir objdir-l10n/merged $AB_CD
+
+#. Finally, repackage using the locale-specific changes.
+
+   .. code-block:: shell
+
+      MOZCONFIG=mozconfig.l10n LOCALE_MERGEDIR=`realpath objdir-l10n/merged` ./mach build -C mobile/android/locales installers-$AB_CD
+
+   (Note the absolute path for ``LOCALE_MERGEDIR``.)  You should find a
+   re-packaged build at ``objdir-l10n/dist/fennec-*$AB_CD*.apk``.
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -164,16 +164,21 @@ AnimationsTimeline.prototype = {
     this.destroySubComponents("details", [{
       event: "frame-selected",
       fn: this.onFrameSelected
     }]);
     this.animationsEl.innerHTML = "";
   },
 
   onWindowResize: function() {
+    // Don't do anything if the root element has a width of 0
+    if (this.rootWrapperEl.offsetWidth === 0) {
+      return;
+    }
+
     if (this.windowResizeTimer) {
       this.win.clearTimeout(this.windowResizeTimer);
     }
 
     this.windowResizeTimer = this.win.setTimeout(() => {
       this.drawHeaderAndBackground();
     }, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
   },
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -73,16 +73,21 @@ exports.createNode = createNode;
  * @param {String} id The ID for the image-element.
  * @param {Number} graphWidth The width of the graph.
  * @param {Number} intervalWidth The width of one interval
  */
 function drawGraphElementBackground(document, id, graphWidth, intervalWidth) {
   let canvas = document.createElement("canvas");
   let ctx = canvas.getContext("2d");
 
+  // Don't do anything if the graph or the intervals have a width of 0
+  if (graphWidth === 0 || intervalWidth === 0) {
+    return;
+  }
+
   // Set the canvas width (as requested) and height (1px, repeated along the Y
   // axis).
   canvas.width = graphWidth;
   canvas.height = 1;
 
   // Create the image data array which will receive the pixels.
   let imageData = ctx.createImageData(canvas.width, canvas.height);
   let pixelArray = imageData.data;
--- a/devtools/client/commandline/test/browser.ini
+++ b/devtools/client/commandline/test/browser.ini
@@ -49,18 +49,18 @@ support-files =
  browser_cmd_csscoverage_page2.html
  browser_cmd_csscoverage_page3.html
  browser_cmd_csscoverage_sheetA.css
  browser_cmd_csscoverage_sheetB.css
  browser_cmd_csscoverage_sheetC.css
  browser_cmd_csscoverage_sheetD.css
 [browser_cmd_folder.js]
 [browser_cmd_highlight_01.js]
-skip-if = os == "linux" && debug # Bug 1210208
 [browser_cmd_highlight_02.js]
+[browser_cmd_highlight_03.js]
 [browser_cmd_inject.js]
 support-files =
  browser_cmd_inject.html
 [browser_cmd_csscoverage_util.js]
 [browser_cmd_jsb.js]
 support-files =
   browser_cmd_jsb_script.jsi
 [browser_cmd_listen.js]
--- a/devtools/client/commandline/test/browser_cmd_highlight_01.js
+++ b/devtools/client/commandline/test/browser_cmd_highlight_01.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests the various highlight command parameters and options
 
 // Creating a test page with many elements to test the --showall option
 var TEST_PAGE = "data:text/html;charset=utf-8,<body><ul>";
-for (let i = 0; i < 200; i ++) {
+for (let i = 0; i < 101; i ++) {
   TEST_PAGE += "<li class='item'>" + i + "</li>";
 }
 TEST_PAGE += "</ul></body>";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
@@ -53,148 +53,52 @@ function* spawnTest() {
         markup: 'VVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
         output: '1 node highlighted'
       }
     },
     {
-      setup: 'highlight body --hide',
-      check: {
-        input:  'highlight body --hide',
-        hints:                       'guides [options]',
-        markup: 'VVVVVVVVVVVVVVVIIIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
       setup: 'highlight body --hideguides',
       check: {
         input:  'highlight body --hideguides',
         hints:                             ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
         output: '1 node highlighted'
       }
     },
     {
-      setup: 'highlight body --show',
-      check: {
-        input:  'highlight body --show',
-        hints:                       'infobar [options]',
-        markup: 'VVVVVVVVVVVVVVVIIIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
       setup: 'highlight body --showinfobar',
       check: {
         input:  'highlight body --showinfobar',
         hints:                              ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
         output: '1 node highlighted'
       }
     },
     {
-      setup: 'highlight body --showa',
-      check: {
-        input:  'highlight body --showa',
-        hints:                        'll [options]',
-        markup: 'VVVVVVVVVVVVVVVIIIIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
       setup: 'highlight body --showall',
       check: {
         input:  'highlight body --showall',
         hints:                          ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
         output: '1 node highlighted'
       }
     },
     {
-      setup: 'highlight body --r',
-      check: {
-        input:  'highlight body --r',
-        hints:                    'egion [options]',
-        markup: 'VVVVVVVVVVVVVVVIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
-      setup: 'highlight body --region',
-      check: {
-        input:  'highlight body --region',
-        hints:                         ' <selection> [options]',
-        markup: 'VVVVVVVVVVVVVVVIIIIIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Value required for \'region\'.'
-      }
-    },
-    {
-      setup: 'highlight body --fi',
-      check: {
-        input:  'highlight body --fi',
-        hints:                     'll [options]',
-        markup: 'VVVVVVVVVVVVVVVIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
-      setup: 'highlight body --fill',
-      check: {
-        input:  'highlight body --fill',
-        hints:                       ' <string> [options]',
-        markup: 'VVVVVVVVVVVVVVVIIIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Value required for \'fill\'.'
-      }
-    },
-    {
-      setup: 'highlight body --ke',
-      check: {
-        input:  'highlight body --ke',
-        hints:                     'ep [options]',
-        markup: 'VVVVVVVVVVVVVVVIIII',
-        status: 'ERROR'
-      },
-      exec: {
-        output: 'Error: Too many arguments'
-      }
-    },
-    {
       setup: 'highlight body --keep',
       check: {
         input:  'highlight body --keep',
         hints:                       ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
@@ -220,38 +124,29 @@ function* spawnTest() {
       setup: 'highlight .item',
       check: {
         input:  'highlight .item',
         hints:                 ' [options]',
         markup: 'VVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
-        output: '200 nodes matched, but only 100 nodes highlighted. Use ' +
+        output: '101 nodes matched, but only 100 nodes highlighted. Use ' +
           '\'--showall\' to show all'
       }
     },
     {
       setup: 'highlight .item --showall',
       check: {
         input:  'highlight .item --showall',
         hints:                           ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID'
       },
       exec: {
-        output: '200 nodes highlighted'
-      }
-    },
-    {
-      setup: 'unhighlight',
-      check: {
-        input:  'unhighlight',
-        hints:  '',
-        markup: 'VVVVVVVVVVV',
-        status: 'VALID'
+        output: '101 nodes highlighted'
       }
     }
   ]);
 
   yield helpers.closeToolbar(options);
   yield helpers.closeTab(options);
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/commandline/test/browser_cmd_highlight_03.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the various highlight command parameters and options that doesn't
+// involve nodes at all.
+
+var TEST_PAGE = "data:text/html;charset=utf-8,";
+
+function test() {
+  return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+  let options = yield helpers.openTab(TEST_PAGE);
+  yield helpers.openToolbar(options);
+
+  yield helpers.audit(options, [
+    {
+      setup: 'highlight body --hide',
+      check: {
+        input:  'highlight body --hide',
+        hints:                       'guides [options]',
+        markup: 'VVVVVVVVVVVVVVVIIIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'highlight body --show',
+      check: {
+        input:  'highlight body --show',
+        hints:                       'infobar [options]',
+        markup: 'VVVVVVVVVVVVVVVIIIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'highlight body --showa',
+      check: {
+        input:  'highlight body --showa',
+        hints:                        'll [options]',
+        markup: 'VVVVVVVVVVVVVVVIIIIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'highlight body --r',
+      check: {
+        input:  'highlight body --r',
+        hints:                    'egion [options]',
+        markup: 'VVVVVVVVVVVVVVVIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'highlight body --region',
+      check: {
+        input:  'highlight body --region',
+        hints:                         ' <selection> [options]',
+        markup: 'VVVVVVVVVVVVVVVIIIIIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Value required for \'region\'.'
+      }
+    },
+    {
+      setup: 'highlight body --fi',
+      check: {
+        input:  'highlight body --fi',
+        hints:                     'll [options]',
+        markup: 'VVVVVVVVVVVVVVVIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'highlight body --fill',
+      check: {
+        input:  'highlight body --fill',
+        hints:                       ' <string> [options]',
+        markup: 'VVVVVVVVVVVVVVVIIIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Value required for \'fill\'.'
+      }
+    },
+    {
+      setup: 'highlight body --ke',
+      check: {
+        input:  'highlight body --ke',
+        hints:                     'ep [options]',
+        markup: 'VVVVVVVVVVVVVVVIIII',
+        status: 'ERROR'
+      },
+      exec: {
+        output: 'Error: Too many arguments'
+      }
+    },
+    {
+      setup: 'unhighlight',
+      check: {
+        input:  'unhighlight',
+        hints:  '',
+        markup: 'VVVVVVVVVVV',
+        status: 'VALID'
+      }
+    }
+  ]);
+
+  yield helpers.closeToolbar(options);
+  yield helpers.closeTab(options);
+}
--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -61,16 +61,17 @@ function SourcesView(controller, Debugge
   this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
   this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
   this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
   this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
   this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
   this._onEditorContextMenuOpen = this._onEditorContextMenuOpen.bind(this);
   this._onCopyUrlCommand = this._onCopyUrlCommand.bind(this);
   this._onNewTabCommand = this._onNewTabCommand.bind(this);
+  this._onConditionalPopupHidden = this._onConditionalPopupHidden.bind(this);
 }
 
 SourcesView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function() {
     dumpn("Initializing the SourcesView");
@@ -108,20 +109,22 @@ SourcesView.prototype = Heritage.extend(
     this._editorContainer.addEventListener("mousedown", this._onMouseDown, false);
 
     this.widget.addEventListener("select", this._onSourceSelect, false);
 
     this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
     this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
     this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
     this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
+    this._cbPanel.addEventListener("popuphidden", this._onConditionalPopupHidden, false);
     this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
     this._copyUrlMenuItem.addEventListener("command", this._onCopyUrlCommand, false);
     this._newTabMenuItem.addEventListener("command", this._onNewTabCommand, false);
 
+    this._cbPanel.hidden = true;
     this.allowFocusOnRightClick = true;
     this.autoFocusOnSelection = false;
     this.autoFocusOnFirstItem = false;
 
     // Sort the contents by the displayed label.
     this.sortContents((aFirst, aSecond) => {
       return +(aFirst.attachment.label.toLowerCase() >
                aSecond.attachment.label.toLowerCase());
@@ -146,16 +149,17 @@ SourcesView.prototype = Heritage.extend(
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
     this.widget.removeEventListener("select", this._onSourceSelect, false);
     this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
     this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
     this._cbPanel.removeEventListener("popupshown", this._onConditionalPopupShown, false);
     this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
+    this._cbPanel.removeEventListener("popuphidden", this._onConditionalPopupHidden, false);
     this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
     this._copyUrlMenuItem.removeEventListener("command", this._onCopyUrlCommand, false);
     this._newTabMenuItem.removeEventListener("command", this._onNewTabCommand, false);
     this.DebuggerView.editor.off("popupOpen", this._onEditorContextMenuOpen, false);
   },
 
   empty: function() {
     WidgetMethods.empty.call(this);
@@ -672,36 +676,47 @@ SourcesView.prototype = Heritage.extend(
    */
   _openConditionalPopup: function() {
     let breakpointItem = this._getBreakpoint(this._selectedBreakpoint);
     let attachment = breakpointItem.attachment;
     // Check if this is an enabled conditional breakpoint, and if so,
     // retrieve the current conditional epression.
     let bp = getBreakpoint(this.getState(), attachment);
     let expr = (bp ? (bp.condition || "") : "");
+    let cbPanel = this._cbPanel;
 
     // Update the conditional expression textbox. If no expression was
     // previously set, revert to using an empty string by default.
     this._cbTextbox.value = expr;
 
-    // Show the conditional expression panel. The popup arrow should be pointing
-    // at the line number node in the breakpoint item view.
-    this._cbPanel.hidden = false;
-    this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
-                            BREAKPOINT_CONDITIONAL_POPUP_POSITION,
-                            BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
-                            BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
+
+    function openPopup() {
+      // Show the conditional expression panel. The popup arrow should be pointing
+      // at the line number node in the breakpoint item view.
+      cbPanel.hidden = false;
+      cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
+                              BREAKPOINT_CONDITIONAL_POPUP_POSITION,
+                              BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
+                              BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
+
+      cbPanel.removeEventListener('popuphidden', openPopup, false);
+    }
+
+    // Wait until the other cb panel is closed
+    if (!this._cbPanel.hidden) {
+      this._cbPanel.addEventListener('popuphidden', openPopup, false);
+    } else {
+      openPopup();
+    }
   },
 
   /**
    * Hides a conditional breakpoint's expression input popup.
    */
   _hideConditionalPopup: function() {
-    this._cbPanel.hidden = true;
-
     // Sometimes this._cbPanel doesn't have hidePopup method which doesn't
     // break anything but simply outputs an exception to the console.
     if (this._cbPanel.hidePopup) {
       this._cbPanel.hidePopup();
     }
   },
 
   /**
@@ -1168,16 +1183,23 @@ SourcesView.prototype = Heritage.extend(
     let bp = this._selectedBreakpoint;
     if (bp) {
       let condition = this._cbTextbox.value;
       this.actions.setBreakpointCondition(bp.location, condition);
     }
   },
 
   /**
+   * The popup hidden listener for the breakpoints conditional expression panel.
+   */
+  _onConditionalPopupHidden: function() {
+    this._cbPanel.hidden = true;
+  },
+
+  /**
    * The keypress listener for the breakpoints conditional expression textbox.
    */
   _onConditionalTextboxKeyPress: function(e) {
     if (e.keyCode == e.DOM_VK_RETURN) {
       this._hideConditionalPopup();
     }
   },
 
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -80,16 +80,17 @@ support-files =
   doc_function-search.html
   doc_global-method-override.html
   doc_iframes.html
   doc_included-script.html
   doc_inline-debugger-statement.html
   doc_inline-script.html
   doc_large-array-buffer.html
   doc_listworkers-tab.html
+  doc_map-set.html
   doc_minified.html
   doc_minified_bogus_map.html
   doc_native-event-handler.html
   doc_no-page-sources.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
@@ -536,18 +537,18 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_variables-view-frame-parameters-03.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-frame-with.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-frozen-sealed-nonext.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-hide-non-enums.js]
-skip-if = e10s && debug
 [browser_dbg_variables-view-large-array-buffer.js]
+[browser_dbg_variables-view-map-set.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-override-01.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-override-02.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-popup-01.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-popup-02.js]
--- a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
@@ -17,103 +17,107 @@ var gEditor, gSources, gPrefs, gOptions,
 var gFirstSourceLabel = "code_ugly-6.js";
 var gSecondSourceLabel = "code_ugly-7.js";
 
 var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
 Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
 
 function test(){
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
-    gTab = aTab;
-    gDebuggee = aDebuggee;
-    gPanel = aPanel;
-    gDebugger = gPanel.panelWin;
-    gEditor = gDebugger.DebuggerView.editor;
-    gSources = gDebugger.DebuggerView.Sources;
-    gPrefs = gDebugger.Prefs;
-    gOptions = gDebugger.DebuggerView.Options;
-    gView = gDebugger.DebuggerView;
+    const gTab = aTab;
+    const gDebuggee = aDebuggee;
+    const gPanel = aPanel;
+    const gDebugger = gPanel.panelWin;
+    const gEditor = gDebugger.DebuggerView.editor;
+    const gSources = gDebugger.DebuggerView.Sources;
+    const gPrefs = gDebugger.Prefs;
+    const gOptions = gDebugger.DebuggerView.Options;
+    const gView = gDebugger.DebuggerView;
 
     // Should be on by default.
     testAutoPrettyPrintOn();
 
-    waitForSourceShown(gPanel, gFirstSourceLabel)
-      .then(testSourceIsUgly)
-      .then(() => waitForSourceShown(gPanel, gFirstSourceLabel))
-      .then(testSourceIsPretty)
-      .then(testPrettyPrintButtonOn)
-      .then(() => {
-        // Switch to the second source.
-        let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN, 2);
-        gSources.selectedIndex = 1;
-        return finished;
-      })
-      .then(testSecondSourceLabel)
-      .then(() => {
-        // Switch back to first source.
-        let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
-        gSources.selectedIndex = 0;
-        return finished;
-      })
-      .then(testFirstSourceLabel)
-      .then(testPrettyPrintButtonOn)
+    Task.spawn(function*() {
+
+      yield waitForSourceShown(gPanel, gFirstSourceLabel);
+      testSourceIsUgly();
+
+      yield waitForSourceShown(gPanel, gFirstSourceLabel);
+      testSourceIsPretty();
+      testPrettyPrintButtonOn();
+
+      // select second source
+      yield selectSecondSource();
+      testSecondSourceLabel();
+
+      // select first source
+      yield selectFirstSource();
+      testFirstSourceLabel();
+      testPrettyPrintButtonOn();
+
       // Disable auto pretty printing so it does not affect the following tests.
-      .then(disableAutoPrettyPrint)
-      .then(() => closeDebuggerAndFinish(gPanel))
-      .then(null, aError => {
-        ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
-      })
+      yield disableAutoPrettyPrint();
+
+      closeDebuggerAndFinish(gPanel)
+        .then(null, aError => {
+          ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
+        })
+    });
+
+    function selectSecondSource() {
+      let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN, 2);
+      gSources.selectedIndex = 1;
+      return finished;
+    }
+
+    function selectFirstSource() {
+      let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
+      gSources.selectedIndex = 0;
+      return finished;
+    }
+
+    function testSourceIsUgly() {
+      ok(!gEditor.getText().includes("\n  "),
+        "The source shouldn't be pretty printed yet.");
+    }
+
+    function testFirstSourceLabel(){
+      let source = gSources.selectedItem.attachment.source;
+      ok(source.url === EXAMPLE_URL + gFirstSourceLabel,
+        "First source url is correct.");
+    }
+
+    function testSecondSourceLabel(){
+      let source = gSources.selectedItem.attachment.source;
+      ok(source.url === EXAMPLE_URL + gSecondSourceLabel,
+        "Second source url is correct.");
+    }
+
+    function testAutoPrettyPrintOn(){
+      is(gPrefs.autoPrettyPrint, true,
+        "The auto-pretty-print pref should be on.");
+      is(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
+        "The Auto pretty print menu item should be checked.");
+    }
+
+    function testPrettyPrintButtonOn(){
+      is(gDebugger.document.getElementById("pretty-print").checked, true,
+        "The button should be checked when the source is selected.");
+    }
+
+    function disableAutoPrettyPrint(){
+      gOptions._autoPrettyPrint.setAttribute("checked", "false");
+      gOptions._toggleAutoPrettyPrint();
+      gOptions._onPopupHidden();
+      info("Disabled auto pretty printing.");
+    }
+
+    function testSourceIsPretty() {
+      ok(gEditor.getText().includes("\n  "),
+        "The source should be pretty printed.")
+    }
+
+    registerCleanupFunction(function() {
+      Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
+    });
+
   });
 }
-
-function testSourceIsUgly() {
-  ok(!gEditor.getText().includes("\n  "),
-    "The source shouldn't be pretty printed yet.");
-}
-
-function testFirstSourceLabel(){
-  let source = gSources.selectedItem.attachment.source;
-  ok(source.url === EXAMPLE_URL + gFirstSourceLabel,
-    "First source url is correct.");
-}
-
-function testSecondSourceLabel(){
-  let source = gSources.selectedItem.attachment.source;
-  ok(source.url === EXAMPLE_URL + gSecondSourceLabel,
-    "Second source url is correct.");
-}
-
-function testAutoPrettyPrintOn(){
-  is(gPrefs.autoPrettyPrint, true,
-    "The auto-pretty-print pref should be on.");
-  is(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
-    "The Auto pretty print menu item should be checked.");
-}
-
-function testPrettyPrintButtonOn(){
-  is(gDebugger.document.getElementById("pretty-print").checked, true,
-    "The button should be checked when the source is selected.");
-}
-
-function disableAutoPrettyPrint(){
-  gOptions._autoPrettyPrint.setAttribute("checked", "false");
-  gOptions._toggleAutoPrettyPrint();
-  gOptions._onPopupHidden();
-  info("Disabled auto pretty printing.");
-}
-
-function testSourceIsPretty() {
-  ok(gEditor.getText().includes("\n  "),
-    "The source should be pretty printed.")
-}
-
-registerCleanupFunction(function() {
-  gTab = null;
-  gDebuggee = null;
-  gPanel = null;
-  gDebugger = null;
-  gEditor = null;
-  gSources = null;
-  gOptions = null;
-  gPrefs = null;
-  gView = null;
-  Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
-});
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-01.js
@@ -1,13 +1,19 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+  "TypeError: this.transport is null");
+
 /**
  * Tests that event listeners aren't fetched when the events tab isn't selected.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html";
 
 function test() {
   initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
--- a/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
@@ -1,13 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+  "Error: Assertion failure: Should have an event loop.");
+
 /**
  * Tests the behavior of the debugger statement.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html";
 
 var gClient;
 var gTab;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
@@ -4,232 +4,227 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests basic functionality of sources filtering (file search).
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
-var gTab, gPanel, gDebugger;
-var gSources, gSearchView, gSearchBox;
-
 function test() {
   // Debug test slaves are a bit slow at this test.
   requestLongerTimeout(3);
 
   initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
-    gTab = aTab;
-    gPanel = aPanel;
-    gDebugger = gPanel.panelWin;
-    gSources = gDebugger.DebuggerView.Sources;
-    gSearchView = gDebugger.DebuggerView.Filtering.FilteredSources;
-    gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+    const gTab = aTab;
+    const gPanel = aPanel;
+    const gDebugger = gPanel.panelWin;
+    const gSources = gDebugger.DebuggerView.Sources;
+    const gSearchView = gDebugger.DebuggerView.Filtering.FilteredSources;
+    const gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+
+    Task.spawn(function*() {
+      // move searches to yields
+      // not sure what to do with the error...
+
+      yield waitForSourceShown(gPanel, "-01.js");
+      yield bogusSearch();
+      yield firstSearch();
+      yield secondSearch();
+      yield thirdSearch();
+      yield fourthSearch();
+      yield fifthSearch();
+      yield sixthSearch();
+      yield seventhSearch();
+
+      return closeDebuggerAndFinish(gPanel)
+        .then(null, aError => {
+          ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        });
+    });
+
+    function bogusSearch() {
+      let finished = promise.all([
+        ensureSourceIs(gPanel, "-01.js"),
+        ensureCaretAt(gPanel, 1),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND)
+      ]);
+
+      setText(gSearchBox, "BOGUS");
+
+      return finished.then(() => promise.all([
+        ensureSourceIs(gPanel, "-01.js"),
+        ensureCaretAt(gPanel, 1),
+        verifyContents({ itemCount: 0, hidden: true })
+      ]));
+    }
+
+    function firstSearch() {
+      let finished = promise.all([
+        ensureSourceIs(gPanel, "-01.js"),
+        ensureCaretAt(gPanel, 1),
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-02.js")
+      ]);
+
+      setText(gSearchBox, "-02.js");
+
+      return finished.then(() => promise.all([
+        ensureSourceIs(gPanel, "-02.js"),
+        ensureCaretAt(gPanel, 1),
+        verifyContents({ itemCount: 1, hidden: false })
+      ]));
+    }
+
+    function secondSearch() {
+      let finished = promise.all([
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-01.js")
+      ])
+      .then(() => {
+        let finished = promise.all([
+          once(gDebugger, "popupshown"),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+          waitForCaretUpdated(gPanel, 5)
+        ])
+        .then(() => promise.all([
+          ensureSourceIs(gPanel, "-01.js"),
+          ensureCaretAt(gPanel, 5),
+          verifyContents({ itemCount: 1, hidden: false })
+        ]));
+
+        typeText(gSearchBox, ":5");
+        return finished;
+      });
+
+      setText(gSearchBox, ".*-01\.js");
+      return finished;
+    }
+
+    function thirdSearch() {
+      let finished = promise.all([
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-02.js")
+      ])
+      .then(() => {
+        let finished = promise.all([
+          once(gDebugger, "popupshown"),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+          waitForCaretUpdated(gPanel, 6, 6)
+        ])
+        .then(() => promise.all([
+          ensureSourceIs(gPanel, "-02.js"),
+          ensureCaretAt(gPanel, 6, 6),
+          verifyContents({ itemCount: 1, hidden: false })
+        ]));
 
-    waitForSourceShown(gPanel, "-01.js")
-      .then(bogusSearch)
-      .then(firstSearch)
-      .then(secondSearch)
-      .then(thirdSearch)
-      .then(fourthSearch)
-      .then(fifthSearch)
-      .then(sixthSearch)
-      .then(seventhSearch)
-      .then(() => closeDebuggerAndFinish(gPanel))
-      .then(null, aError => {
-        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+        typeText(gSearchBox, "#deb");
+        return finished;
+      });
+
+      setText(gSearchBox, ".*-02\.js");
+      return finished;
+    }
+
+    function fourthSearch() {
+      let finished = promise.all([
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-01.js")
+      ])
+      .then(() => {
+        let finished = promise.all([
+          once(gDebugger, "popupshown"),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+          waitForCaretUpdated(gPanel, 2, 9),
+        ])
+        .then(() => promise.all([
+          ensureSourceIs(gPanel, "-01.js"),
+          ensureCaretAt(gPanel, 2, 9),
+          verifyContents({ itemCount: 1, hidden: false })
+          // ...because we simply searched for ":" in the current file.
+        ]));
+
+        typeText(gSearchBox, "#:"); // # has precedence.
+        return finished;
       });
+
+      setText(gSearchBox, ".*-01\.js");
+      return finished;
+    }
+
+    function fifthSearch() {
+      let finished = promise.all([
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-02.js")
+      ])
+      .then(() => {
+        let finished = promise.all([
+          once(gDebugger, "popuphidden"),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND),
+          waitForCaretUpdated(gPanel, 1, 3)
+        ])
+        .then(() => promise.all([
+          ensureSourceIs(gPanel, "-02.js"),
+          ensureCaretAt(gPanel, 1, 3),
+          verifyContents({ itemCount: 0, hidden: true })
+          // ...because the searched label includes ":5", so nothing is found.
+        ]));
+
+        typeText(gSearchBox, ":5#*"); // # has precedence.
+        return finished;
+      });
+
+      setText(gSearchBox, ".*-02\.js");
+      return finished;
+    }
+
+    function sixthSearch() {
+      let finished = promise.all([
+        ensureSourceIs(gPanel, "-02.js"),
+        ensureCaretAt(gPanel, 1, 3),
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForCaretUpdated(gPanel, 5)
+      ]);
+
+      backspaceText(gSearchBox, 2);
+
+      return finished.then(() => promise.all([
+        ensureSourceIs(gPanel, "-02.js"),
+        ensureCaretAt(gPanel, 5),
+        verifyContents({ itemCount: 1, hidden: false })
+      ]));
+    }
+
+    function seventhSearch() {
+      let finished = promise.all([
+        ensureSourceIs(gPanel, "-02.js"),
+        ensureCaretAt(gPanel, 5),
+        once(gDebugger, "popupshown"),
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
+        waitForSourceShown(gPanel, "-01.js"),
+      ]);
+
+      backspaceText(gSearchBox, 6);
+
+      return finished.then(() => promise.all([
+        ensureSourceIs(gPanel, "-01.js"),
+        ensureCaretAt(gPanel, 1),
+        verifyContents({ itemCount: 2, hidden: false })
+      ]));
+    }
+
+    function verifyContents(aArgs) {
+      is(gSources.visibleItems.length, 2,
+        "The unmatched sources in the widget should not be hidden.");
+      is(gSearchView.itemCount, aArgs.itemCount,
+        "No sources should be displayed in the sources container after a bogus search.");
+      is(gSearchView.hidden, aArgs.hidden,
+        "No sources should be displayed in the sources container after a bogus search.");
+    }
+
   });
 }
-
-function bogusSearch() {
-  let finished = promise.all([
-    ensureSourceIs(gPanel, "-01.js"),
-    ensureCaretAt(gPanel, 1),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND)
-  ]);
-
-  setText(gSearchBox, "BOGUS");
-
-  return finished.then(() => promise.all([
-    ensureSourceIs(gPanel, "-01.js"),
-    ensureCaretAt(gPanel, 1),
-    verifyContents({ itemCount: 0, hidden: true })
-  ]));
-}
-
-function firstSearch() {
-  let finished = promise.all([
-    ensureSourceIs(gPanel, "-01.js"),
-    ensureCaretAt(gPanel, 1),
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-02.js")
-  ]);
-
-  setText(gSearchBox, "-02.js");
-
-  return finished.then(() => promise.all([
-    ensureSourceIs(gPanel, "-02.js"),
-    ensureCaretAt(gPanel, 1),
-    verifyContents({ itemCount: 1, hidden: false })
-  ]));
-}
-
-function secondSearch() {
-  let finished = promise.all([
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-01.js")
-  ])
-  .then(() => {
-    let finished = promise.all([
-      once(gDebugger, "popupshown"),
-      waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-      waitForCaretUpdated(gPanel, 5)
-    ])
-    .then(() => promise.all([
-      ensureSourceIs(gPanel, "-01.js"),
-      ensureCaretAt(gPanel, 5),
-      verifyContents({ itemCount: 1, hidden: false })
-    ]));
-
-    typeText(gSearchBox, ":5");
-    return finished;
-  });
-
-  setText(gSearchBox, ".*-01\.js");
-  return finished;
-}
-
-function thirdSearch() {
-  let finished = promise.all([
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-02.js")
-  ])
-  .then(() => {
-    let finished = promise.all([
-      once(gDebugger, "popupshown"),
-      waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-      waitForCaretUpdated(gPanel, 6, 6)
-    ])
-    .then(() => promise.all([
-      ensureSourceIs(gPanel, "-02.js"),
-      ensureCaretAt(gPanel, 6, 6),
-      verifyContents({ itemCount: 1, hidden: false })
-    ]));
-
-    typeText(gSearchBox, "#deb");
-    return finished;
-  });
-
-  setText(gSearchBox, ".*-02\.js");
-  return finished;
-}
-
-function fourthSearch() {
-  let finished = promise.all([
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-01.js")
-  ])
-  .then(() => {
-    let finished = promise.all([
-      once(gDebugger, "popupshown"),
-      waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-      waitForCaretUpdated(gPanel, 2, 9),
-    ])
-    .then(() => promise.all([
-      ensureSourceIs(gPanel, "-01.js"),
-      ensureCaretAt(gPanel, 2, 9),
-      verifyContents({ itemCount: 1, hidden: false })
-      // ...because we simply searched for ":" in the current file.
-    ]));
-
-    typeText(gSearchBox, "#:"); // # has precedence.
-    return finished;
-  });
-
-  setText(gSearchBox, ".*-01\.js");
-  return finished;
-}
-
-function fifthSearch() {
-  let finished = promise.all([
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-02.js")
-  ])
-  .then(() => {
-    let finished = promise.all([
-      once(gDebugger, "popuphidden"),
-      waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND),
-      waitForCaretUpdated(gPanel, 1, 3)
-    ])
-    .then(() => promise.all([
-      ensureSourceIs(gPanel, "-02.js"),
-      ensureCaretAt(gPanel, 1, 3),
-      verifyContents({ itemCount: 0, hidden: true })
-      // ...because the searched label includes ":5", so nothing is found.
-    ]));
-
-    typeText(gSearchBox, ":5#*"); // # has precedence.
-    return finished;
-  });
-
-  setText(gSearchBox, ".*-02\.js");
-  return finished;
-}
-
-function sixthSearch() {
-  let finished = promise.all([
-    ensureSourceIs(gPanel, "-02.js"),
-    ensureCaretAt(gPanel, 1, 3),
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForCaretUpdated(gPanel, 5)
-  ]);
-
-  backspaceText(gSearchBox, 2);
-
-  return finished.then(() => promise.all([
-    ensureSourceIs(gPanel, "-02.js"),
-    ensureCaretAt(gPanel, 5),
-    verifyContents({ itemCount: 1, hidden: false })
-  ]));
-}
-
-function seventhSearch() {
-  let finished = promise.all([
-    ensureSourceIs(gPanel, "-02.js"),
-    ensureCaretAt(gPanel, 5),
-    once(gDebugger, "popupshown"),
-    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND),
-    waitForSourceShown(gPanel, "-01.js"),
-  ]);
-
-  backspaceText(gSearchBox, 6);
-
-  return finished.then(() => promise.all([
-    ensureSourceIs(gPanel, "-01.js"),
-    ensureCaretAt(gPanel, 1),
-    verifyContents({ itemCount: 2, hidden: false })
-  ]));
-}
-
-function verifyContents(aArgs) {
-  is(gSources.visibleItems.length, 2,
-    "The unmatched sources in the widget should not be hidden.");
-  is(gSearchView.itemCount, aArgs.itemCount,
-    "No sources should be displayed in the sources container after a bogus search.");
-  is(gSearchView.hidden, aArgs.hidden,
-    "No sources should be displayed in the sources container after a bogus search.");
-}
-
-registerCleanupFunction(function() {
-  gTab = null;
-  gPanel = null;
-  gDebugger = null;
-  gSources = null;
-  gSearchView = null;
-  gSearchBox = null;
-});
--- a/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
@@ -1,13 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+
 /**
  * Tests that debuggee scripts are terminated on tab closure.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_terminate-on-tab-close.html";
 
 function test() {
   initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
--- a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
@@ -3,222 +3,247 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Make sure that the variables view remains responsive when faced with
  * huge ammounts of data.
  */
 
+"use strict";
+
 const TAB_URL = EXAMPLE_URL + "doc_large-array-buffer.html";
 
 var gTab, gPanel, gDebugger;
 var gVariables, gEllipsis;
 
 function test() {
+  // this test does a lot of work on large objects, default 45s is not enough
+  requestLongerTimeout(4);
+
   initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gVariables = gDebugger.DebuggerView.Variables;
     gEllipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
 
-    waitForSourceAndCaretAndScopes(gPanel, ".html", 23)
-      .then(() => initialChecks())
-      .then(() => verifyFirstLevel())
-      .then(() => verifyNextLevels())
+    waitForSourceAndCaretAndScopes(gPanel, ".html", 28)
+      .then(() => performTests())
       .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
-      .then(null, aError => {
-        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+      .then(null, error => {
+        ok(false, "Got an error: " + error.message + "\n" + error.stack);
       });
 
     generateMouseClickInTab(gTab, "content.document.querySelector('button')");
   });
 }
 
-function initialChecks() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let bufferVar = localScope.get("buffer");
-  let arrayVar = localScope.get("largeArray");
-  let objectVar = localScope.get("largeObject");
+const VARS_TO_TEST = [
+  {
+    varName: "buffer",
+    stringified: "ArrayBuffer",
+    doNotExpand: true
+  },
+  {
+    varName: "largeArray",
+    stringified: "Int8Array[10000]",
+    extraProps: [
+      [ "buffer", "ArrayBuffer" ],
+      [ "byteLength", "10000" ],
+      [ "byteOffset", "0" ],
+      [ "length", "10000" ],
+      [ "__proto__", "Int8ArrayPrototype" ]
+    ]
+  },
+  {
+    varName: "largeObject",
+    stringified: "Object[10000]",
+    extraProps: [
+      [ "__proto__", "Object" ]
+    ]
+  },
+  {
+    varName: "largeMap",
+    stringified: "Map[10000]",
+    hasEntries: true,
+    extraProps: [
+      [ "size", "10000" ],
+      [ "__proto__", "Object" ]
+    ]
+  },
+  {
+    varName: "largeSet",
+    stringified: "Set[10000]",
+    hasEntries: true,
+    extraProps: [
+      [ "size", "10000" ],
+      [ "__proto__", "Object" ]
+    ]
+  }
+];
 
-  ok(bufferVar, "There should be a 'buffer' variable present in the scope.");
-  ok(arrayVar, "There should be a 'largeArray' variable present in the scope.");
-  ok(objectVar, "There should be a 'largeObject' variable present in the scope.");
-
-  is(bufferVar.target.querySelector(".name").getAttribute("value"), "buffer",
-    "Should have the right property name for 'buffer'.");
-  is(bufferVar.target.querySelector(".value").getAttribute("value"), "ArrayBuffer",
-    "Should have the right property value for 'buffer'.");
-  ok(bufferVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'buffer'.");
+const PAGE_RANGES = [
+  [0, 2499], [2500, 4999], [5000, 7499], [7500, 9999]
+];
 
-  is(arrayVar.target.querySelector(".name").getAttribute("value"), "largeArray",
-    "Should have the right property name for 'largeArray'.");
-  is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array[10000]",
-    "Should have the right property value for 'largeArray'.");
-  ok(arrayVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'largeArray'.");
+function toPageNames(ranges) {
+  return ranges.map(([ from, to ]) => "[" + from + gEllipsis + to + "]");
+}
+
+function performTests() {
+  let localScope = gVariables.getScopeAtIndex(0);
+
+  return promise.all(VARS_TO_TEST.map(spec => {
+    let { varName, stringified, doNotExpand } = spec;
+
+    let variable = localScope.get(varName);
+    ok(variable,
+      `There should be a '${varName}' variable present in the scope.`);
+
+    is(variable.target.querySelector(".name").getAttribute("value"), varName,
+      `Should have the right property name for '${varName}'.`);
+    is(variable.target.querySelector(".value").getAttribute("value"), stringified,
+      `Should have the right property value for '${varName}'.`);
+    ok(variable.target.querySelector(".value").className.includes("token-other"),
+      `Should have the right token class for '${varName}'.`);
+
+    is(variable.expanded, false,
+      `The '${varName}' variable shouldn't be expanded.`);
 
-  is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject",
-    "Should have the right property name for 'largeObject'.");
-  is(objectVar.target.querySelector(".value").getAttribute("value"), "Object[10000]",
-    "Should have the right property value for 'largeObject'.");
-  ok(objectVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'largeObject'.");
+    if (doNotExpand) {
+      return promise.resolve();
+    }
+
+    return variable.expand()
+      .then(() => verifyFirstLevel(variable, spec));
+  }));
+}
 
-  is(bufferVar.expanded, false,
-    "The 'buffer' variable shouldn't be expanded.");
-  is(arrayVar.expanded, false,
-    "The 'largeArray' variable shouldn't be expanded.");
-  is(objectVar.expanded, false,
-    "The 'largeObject' variable shouldn't be expanded.");
+// In objects and arrays, the sliced pages are at the top-level of
+// the expanded object, but with Maps and Sets, we have to expand
+// <entries> first and look there.
+function getExpandedPages(variable, hasEntries) {
+  let expandedPages = promise.defer();
+  if (hasEntries) {
+    let entries = variable.get("<entries>");
+    ok(entries, "<entries> retrieved");
+    entries.expand().then(() => expandedPages.resolve(entries));
+  } else {
+    expandedPages.resolve(variable);
+  }
 
-  return promise.all([arrayVar.expand(),objectVar.expand()]);
+  return expandedPages.promise;
 }
 
-function verifyFirstLevel() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let arrayVar = localScope.get("largeArray");
-  let objectVar = localScope.get("largeObject");
+function verifyFirstLevel(variable, spec) {
+  let { varName, hasEntries, extraProps } = spec;
 
-  let arrayEnums = arrayVar.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let arrayNonEnums = arrayVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(arrayEnums.length, 0,
-    "The 'largeArray' shouldn't contain any enumerable elements.");
-  is(arrayNonEnums.length, 9,
-    "The 'largeArray' should contain all the created non-enumerable elements.");
+  let enums = variable._enum.childNodes;
+  let nonEnums = variable._nonenum.childNodes;
 
-  let objectEnums = objectVar.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let objectNonEnums = objectVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(objectEnums.length, 0,
-    "The 'largeObject' shouldn't contain any enumerable elements.");
-  is(objectNonEnums.length, 5,
-    "The 'largeObject' should contain all the created non-enumerable elements.");
+  is(enums.length, hasEntries ? 1 : 4,
+    `The '${varName}' contains the right number of enumerable elements.`);
+  is(nonEnums.length, extraProps.length,
+    `The '${varName}' contains the right number of non-enumerable elements.`);
 
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[0" + gEllipsis + "2499]", "The first page in the 'largeArray' is named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
-    "", "The first page in the 'largeArray' should not have a corresponding value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    "[2500" + gEllipsis + "4999]", "The second page in the 'largeArray' is named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
-    "", "The second page in the 'largeArray' should not have a corresponding value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    "[5000" + gEllipsis + "7499]", "The third page in the 'largeArray' is named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
-    "", "The third page in the 'largeArray' should not have a corresponding value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeArray' is named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
-    "", "The fourth page in the 'largeArray' should not have a corresponding value.");
+  // the sliced pages begin after <entries> row
+  let pagesOffset = hasEntries ? 1 : 0;
+  let expandedPages = getExpandedPages(variable, hasEntries);
+
+  return expandedPages.then((pagesList) => {
+    toPageNames(PAGE_RANGES).forEach((pageName, i) => {
+      let index = i + pagesOffset;
 
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[0" + gEllipsis + "2499]", "The first page in the 'largeObject' is named correctly.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
-    "", "The first page in the 'largeObject' should not have a corresponding value.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    "[2500" + gEllipsis + "4999]", "The second page in the 'largeObject' is named correctly.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
-    "", "The second page in the 'largeObject' should not have a corresponding value.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    "[5000" + gEllipsis + "7499]", "The thrid page in the 'largeObject' is named correctly.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
-    "", "The thrid page in the 'largeObject' should not have a corresponding value.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeObject' is named correctly.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
-    "", "The fourth page in the 'largeObject' should not have a corresponding value.");
+      is(pagesList.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+        pageName, `The page #${i + 1} in the '${varName}' is named correctly.`);
+      is(pagesList.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+        "", `The page #${i + 1} in the '${varName}' should not have a corresponding value.`);
+    });
+  }).then(() => {
+    extraProps.forEach(([ propName, propValue ], i) => {
+      // the extra props start after the 4 pages
+      let index = i + pagesOffset + 4;
 
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
-    "buffer", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
-    "ArrayBuffer", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
-    "byteLength", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
-    "byteOffset", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
-    "0", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
-    "length", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
+      is(variable.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+        propName, `The other properties in '${varName}' are named correctly.`);
+      is(variable.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+        propValue, `The other properties in '${varName}' have the correct value.`);
+    });
+  }).then(() => verifyNextLevels(variable, spec));
+}
 
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"),
-    "__proto__", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"),
-    "Int8ArrayPrototype", "The other properties 'largeArray' have the correct value.");
+function verifyNextLevels(variable, spec) {
+  let { varName, hasEntries } = spec;
+
+  // the entries are already expanded in verifyFirstLevel
+  let pagesList = hasEntries ? variable.get("<entries>") : variable;
 
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
-    "__proto__", "The other properties 'largeObject' are named correctly.");
-  is(objectVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
-    "Object", "The other properties 'largeObject' have the correct value.");
+  let lastPage = pagesList.get(toPageNames(PAGE_RANGES)[3]);
+  ok(lastPage, `The last page in the 1st level of '${varName}' was retrieved successfully.`);
+
+  return lastPage.expand()
+    .then(() => verifyNextLevels2(lastPage, varName));
 }
 
-function verifyNextLevels() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let objectVar = localScope.get("largeObject");
+function verifyNextLevels2(lastPage1, varName) {
+  const PAGE_RANGES_IN_LAST_PAGE = [
+    [7500, 8124], [8125, 8749], [8750, 9374], [9375, 9999]
+  ];
+
+  let pageEnums1 = lastPage1._enum.childNodes;
+  let pageNonEnums1 = lastPage1._nonenum.childNodes;
+  is(pageEnums1.length, 4,
+    `The last page in the 1st level of '${varName}' should contain all the created enumerable elements.`);
+  is(pageNonEnums1.length, 0,
+    `The last page in the 1st level of '${varName}' should not contain any non-enumerable elements.`);
 
-  let lastPage1 = objectVar.get("[7500" + gEllipsis + "9999]");
-  ok(lastPage1, "The last page in the first level was retrieved successfully.");
-  return lastPage1.expand()
-                  .then(verifyNextLevels2.bind(null, lastPage1));
+  let pageNames = toPageNames(PAGE_RANGES_IN_LAST_PAGE);
+  pageNames.forEach((pageName, i) => {
+    is(lastPage1._enum.querySelectorAll(".variables-view-property .name")[i].getAttribute("value"),
+      pageName, `The page #${i + 1} in the 2nd level of '${varName}' is named correctly.`);
+  });
+
+  let lastPage2 = lastPage1.get(pageNames[3]);
+  ok(lastPage2, "The last page in the 2nd level was retrieved successfully.");
+
+  return lastPage2.expand()
+    .then(() => verifyNextLevels3(lastPage2, varName));
 }
 
-function verifyNextLevels2(lastPage1) {
-  let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(pageEnums1.length, 0,
-    "The last page in the first level shouldn't contain any enumerable elements.");
-  is(pageNonEnums1.length, 4,
-    "The last page in the first level should contain all the created non-enumerable elements.");
+function verifyNextLevels3(lastPage2, varName) {
+  let pageEnums2 = lastPage2._enum.childNodes;
+  let pageNonEnums2 = lastPage2._nonenum.childNodes;
+  is(pageEnums2.length, 625,
+    `The last page in the 3rd level of '${varName}' should contain all the created enumerable elements.`);
+  is(pageNonEnums2.length, 0,
+    `The last page in the 3rd level of '${varName}' shouldn't contain any non-enumerable elements.`);
 
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[7500" + gEllipsis + "8124]", "The first page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    "[8125" + gEllipsis + "8749]", "The second page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    "[8750" + gEllipsis + "9374]", "The third page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    "[9375" + gEllipsis + "9999]", "The fourth page in this level named correctly (1).");
-
-  let lastPage2 = lastPage1.get("[9375" + gEllipsis + "9999]");
-  ok(lastPage2, "The last page in the second level was retrieved successfully.");
-  return lastPage2.expand()
-                  .then(verifyNextLevels3.bind(null, lastPage2));
-}
+  const LEAF_ITEMS = [
+    [0, 9375, 624],
+    [1, 9376, 623],
+    [623, 9998, 1],
+    [624, 9999, 0]
+  ];
 
-function verifyNextLevels3(lastPage2) {
-  let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(pageEnums2.length, 625,
-    "The last page in the third level should contain all the created enumerable elements.");
-  is(pageNonEnums2.length, 0,
-    "The last page in the third level shouldn't contain any non-enumerable elements.");
+  function expectedValue(name, value) {
+    switch (varName) {
+      case "largeArray": return 0;
+      case "largeObject": return value;
+      case "largeMap": return name + " \u2192 " + value;
+      case "largeSet": return value;
+    }
+  }
 
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    9375, "The properties in this level are named correctly (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    9376, "The properties in this level are named correctly (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[623].getAttribute("value"),
-    9998, "The properties in this level are named correctly (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[624].getAttribute("value"),
-    9999, "The properties in this level are named correctly (3).");
-
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
-    624, "The properties in this level have the correct value (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
-    623, "The properties in this level have the correct value (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[623].getAttribute("value"),
-    1, "The properties in this level have the correct value (3).");
-  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[624].getAttribute("value"),
-    0, "The properties in this level have the correct value (3).");
+  LEAF_ITEMS.forEach(([index, name, value]) => {
+    is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+      name, `The properties in the leaf level of '${varName}' are named correctly.`);
+    is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+      expectedValue(name, value), `The properties in the leaf level of '${varName}' have the correct value.`);
+  });
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gPanel = null;
   gDebugger = null;
   gVariables = null;
   gEllipsis = null;
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that Map and Set and their Weak friends are displayed in variables view.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_map-set.html";
+
+var test = Task.async(function*() {
+  const [tab,, panel] = yield initDebugger(TAB_URL);
+  yield ensureSourceIs(panel, "doc_map-set.html", true);
+
+  const scopes = waitForCaretAndScopes(panel, 37);
+  callInTab(tab, "startTest");
+  yield scopes;
+
+  const variables = panel.panelWin.DebuggerView.Variables;
+  ok(variables, "Should get the variables view.");
+
+  const scope = variables.getScopeAtIndex(0);
+  ok(scope, "Should get the current function's scope.");
+
+  /* Test the maps */
+  for (let varName of ["map", "weakMap"]) {
+    const mapVar = scope.get(varName);
+    ok(mapVar, `Retrieved the '${varName}' variable from the scope`);
+
+    info(`Expanding '${varName}' variable`);
+    yield mapVar.expand();
+
+    const entries = mapVar.get("<entries>");
+    ok(entries, `Retrieved the '${varName}' entries`);
+
+    info(`Expanding '${varName}' entries`);
+    yield entries.expand();
+
+    // Check the entries. WeakMap returns its entries in a nondeterministic
+    // order, so we make our job easier by not testing the exact values.
+    let i = 0;
+    for (let [ name, entry ] of entries) {
+      is(name, i, `The '${varName}' entry's property name is correct`);
+      ok(entry.displayValue.startsWith("Object \u2192 "),
+        `The '${varName}' entry's property value is correct`);
+      yield entry.expand();
+
+      let key = entry.get("key");
+      ok(key, `The '${varName}' entry has the 'key' property`);
+      yield key.expand();
+
+      let keyProperty = key.get("a");
+      ok(keyProperty,
+        `The '${varName}' entry's 'key' has the correct property`);
+
+      let value = entry.get("value");
+      ok(value, `The '${varName}' entry has the 'value' property`);
+
+      i++;
+    }
+
+    is(i, 2, `The '${varName}' entry count is correct`);
+
+    // Check the extra property on the object
+    let extraProp = mapVar.get("extraProp");
+    ok(extraProp, `Retrieved the '${varName}' extraProp`);
+    is(extraProp.displayValue, "true",
+      `The '${varName}' extraProp's value is correct`);
+  }
+
+  /* Test the sets */
+  for (let varName of ["set", "weakSet"]) {
+    const setVar = scope.get(varName);
+    ok(setVar, `Retrieved the '${varName}' variable from the scope`);
+
+    info(`Expanding '${varName}' variable`);
+    yield setVar.expand();
+
+    const entries = setVar.get("<entries>");
+    ok(entries, `Retrieved the '${varName}' entries`);
+
+    info(`Expanding '${varName}' entries`);
+    yield entries.expand();
+
+    // Check the entries. WeakSet returns its entries in a nondeterministic
+    // order, so we make our job easier by not testing the exact values.
+    let i = 0;
+    for (let [ name, entry ] of entries) {
+      is(name, i, `The '${varName}' entry's property name is correct`);
+      is(entry.displayValue, "Object",
+        `The '${varName}' entry's property value is correct`);
+      yield entry.expand();
+
+      let entryProperty = entry.get("a");
+      ok(entryProperty,
+        `The '${varName}' entry's value has the correct property`);
+
+      i++;
+    }
+
+    is(i, 2, `The '${varName}' entry count is correct`);
+
+    // Check the extra property on the object
+    let extraProp = setVar.get("extraProp");
+    ok(extraProp, `Retrieved the '${varName}' extraProp`);
+    is(extraProp.displayValue, "true",
+      `The '${varName}' extraProp's value is correct`);
+  }
+
+  resumeDebuggerThenCloseAndFinish(panel);
+});
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -1,11 +1,15 @@
 // Check to make sure that a worker can be attached to a toolbox
 // directly, and that the toolbox has expected properties.
 
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+
 var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
 var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
 
 add_task(function* () {
   yield pushPrefs(["devtools.scratchpad.enabled", true]);
 
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
--- a/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
+++ b/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
@@ -11,17 +11,22 @@
   <body>
     <button onclick="test(10000)">Click me!</button>
 
     <script type="text/javascript">
       function test(aNumber) {
         var buffer = new ArrayBuffer(aNumber);
         var largeArray = new Int8Array(buffer);
         var largeObject = {};
+        var largeMap = new Map();
+        var largeSet = new Set();
 
         for (var i = 0; i < aNumber; i++) {
-          largeObject[i] = aNumber - i - 1;
+          let value = aNumber - i - 1;
+          largeObject[i] = value;
+          largeMap.set(i, value);
+          largeSet.add(value);
         }
         debugger;
       }
     </script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_map-set.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page for Maps and Sets</title>
+  </head>
+
+  <body>
+    <script>
+      function startTest() {
+        let obj0 = { a: 0 };
+        let obj1 = { a: 1 };
+
+        let map = new Map();
+        map.set(obj0, 0);
+        map.set(obj1, 1);
+        map.extraProp = true;
+
+        let weakMap = new WeakMap();
+        weakMap.set(obj0, 0);
+        weakMap.set(obj1, 1);
+        weakMap.extraProp = true;
+
+        let set = new Set();
+        set.add(obj0);
+        set.add(obj1);
+        set.extraProp = true;
+
+        let weakSet = new WeakSet();
+        weakSet.add(obj0);
+        weakSet.add(obj1);
+        weakSet.extraProp = true;
+
+        debugger;
+      };
+    </script>
+  </body>
+
+</html>
--- a/devtools/client/devtools-startup.js
+++ b/devtools/client/devtools-startup.js
@@ -4,18 +4,18 @@
 
 /* FIXME: remove this globals comment and replace with import-globals-from when
    bug 1242893 is fixed */
 /* globals BrowserToolboxProcess */
 
 /**
  * This XPCOM component is loaded very early.
  * It handles command line arguments like -jsconsole, but also ensures starting
- * core modules like devtools/devtools-browser that listen for application
- * startup.
+ * core modules like 'devtools-browser.js' that hooks the browser windows
+ * and ensure setting up tools.
  *
  * Be careful to lazy load dependencies as much as possible.
  **/
 
 "use strict";
 
 const { interfaces: Ci, utils: Cu } = Components;
 const kDebuggerPrefs = [
@@ -62,22 +62,20 @@ DevToolsStartup.prototype = {
         this.handleDevToolsFlag(window);
       }
     }.bind(this);
     Services.obs.addObserver(onStartup, "browser-delayed-startup-finished",
                              false);
   },
 
   initDevTools: function() {
-    let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    let { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     // Ensure loading main devtools module that hooks up into browser UI
     // and initialize all devtools machinery.
-    // browser.xul or main top-level document used to load this module,
-    // but this code may be called without/before it.
-    require("devtools/client/framework/devtools-browser");
+    loader.main("devtools/client/main");
   },
 
   handleConsoleFlag: function(cmdLine) {
     let window = Services.wm.getMostRecentWindow("devtools:webconsole");
     if (!window) {
       this.initDevTools();
 
       let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/browser-menus.js
@@ -0,0 +1,471 @@
+/* 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";
+
+/**
+ * This module inject dynamically menu items and key shortcuts into browser UI.
+ *
+ * Menu and shortcut definitions are fetched from:
+ * - devtools/client/menus for top level entires
+ * - devtools/client/definitions for tool-specifics entries
+ */
+
+const Services = require("Services");
+const MenuStrings = Services.strings.createBundle("chrome://devtools/locale/menus.properties");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+// Keep list of inserted DOM Elements in order to remove them on unload
+// Maps browser xul document => list of DOM Elements
+const FragmentsCache = new Map();
+
+function l10n(key) {
+  return MenuStrings.GetStringFromName(key);
+}
+
+/**
+ * Create a xul:key element
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys are to be added.
+ * @param {String} l10nKey
+ *        Prefix of the properties entry to look for key shortcut in
+ *        localization file. We will look for {property}.key and
+ *        {property}.keytext for non-character shortcuts like F12.
+ * @param {String} command
+ *        Id of the xul:command to map to.
+ * @param {Object} key definition dictionnary
+ *        Definition with following attributes:
+ *        - {String} id
+ *          xul:key's id, automatically prefixed with "key_",
+ *        - {String} modifiers
+ *          Space separater list of modifier names,
+ *        - {Boolean} keytext
+ *          If true, consider the shortcut as a characther one,
+ *          otherwise a non-character one like F12.
+ *
+ * @return XULKeyElement
+ */
+function createKey(doc, l10nKey, command, key) {
+  let k = doc.createElement("key");
+  k.id = "key_" + key.id;
+  let shortcut = l10n(l10nKey + ".key");
+  if (shortcut.startsWith("VK_")) {
+    k.setAttribute("keycode", shortcut);
+    k.setAttribute("keytext", l10n(l10nKey + ".keytext"));
+  } else {
+    k.setAttribute("key", shortcut);
+  }
+  if (command) {
+    k.setAttribute("command", command);
+  }
+  if (key.modifiers) {
+    k.setAttribute("modifiers", key.modifiers);
+  }
+  return k;
+}
+
+/**
+ * Create a xul:menuitem element
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys are to be added.
+ * @param {String} id
+ *        Element id.
+ * @param {String} label
+ *        Menu label.
+ * @param {String} broadcasterId (optional)
+ *        Id of the xul:broadcaster to map to.
+ * @param {String} accesskey (optional)
+ *        Access key of the menuitem, used as shortcut while opening the menu.
+ * @param {Boolean} isCheckbox
+ *        If true, the menuitem will act as a checkbox and have an optional
+ *        tick on its left.
+ *
+ * @return XULMenuItemElement
+ */
+function createMenuItem({ doc, id, label, broadcasterId, accesskey, isCheckbox }) {
+  let menuitem = doc.createElement("menuitem");
+  menuitem.id = id;
+  if (label) {
+    menuitem.setAttribute("label", label);
+  }
+  if (broadcasterId) {
+    menuitem.setAttribute("observes", broadcasterId);
+  }
+  if (accesskey) {
+    menuitem.setAttribute("accesskey", accesskey);
+  }
+  if (isCheckbox) {
+    menuitem.setAttribute("type", "checkbox");
+    menuitem.setAttribute("autocheck", "false");
+  }
+  return menuitem;
+}
+
+/**
+ * Create a xul:broadcaster element
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys are to be added.
+ * @param {String} id
+ *        Element id.
+ * @param {String} label
+ *        Broadcaster label.
+ * @param {Boolean} isCheckbox
+ *        If true, the broadcaster is a checkbox one.
+ *
+ * @return XULMenuItemElement
+ */
+function createBroadcaster({ doc, id, label, isCheckbox }) {
+  let broadcaster = doc.createElement("broadcaster");
+  broadcaster.id = id;
+  broadcaster.setAttribute("label", label);
+  if (isCheckbox) {
+    broadcaster.setAttribute("type", "checkbox");
+    broadcaster.setAttribute("autocheck", "false");
+  }
+  return broadcaster;
+}
+
+/**
+ * Create a xul:command element
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys are to be added.
+ * @param {String} id
+ *        Element id.
+ * @param {String} oncommand
+ *        JS String to run when the command is fired.
+ * @param {Boolean} disabled
+ *        If true, the command is disabled and hidden.
+ *
+ * @return XULCommandElement
+ */
+function createCommand({ doc, id, oncommand, disabled }) {
+  let command = doc.createElement("command");
+  command.id = id;
+  command.setAttribute("oncommand", oncommand);
+  if (disabled) {
+    command.setAttribute("disabled", "true");
+    command.setAttribute("hidden", "true");
+  }
+  return command;
+}
+
+/**
+ * Add a <key> to <keyset id="devtoolsKeyset">.
+ * Appending a <key> element is not always enough. The <keyset> needs
+ * to be detached and reattached to make sure the <key> is taken into
+ * account (see bug 832984).
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys are to be added
+ * @param {XULElement} or {DocumentFragment} keys
+ *        Keys to add
+ */
+function attachKeybindingsToBrowser(doc, keys) {
+  let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
+
+  if (!devtoolsKeyset) {
+    devtoolsKeyset = doc.createElement("keyset");
+    devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
+  }
+  devtoolsKeyset.appendChild(keys);
+  let mainKeyset = doc.getElementById("mainKeyset");
+  mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
+}
+
+/**
+ * Add a menu entry for a tool definition
+ *
+ * @param {Object} toolDefinition
+ *        Tool definition of the tool to add a menu entry.
+ * @param {XULDocument} doc
+ *        The document to which the tool menu item is to be added.
+ */
+function createToolMenuElements(toolDefinition, doc) {
+  let id = toolDefinition.id;
+
+  // Prevent multiple entries for the same tool.
+  if (doc.getElementById("Tools:" + id)) {
+    return;
+  }
+
+  let cmd = createCommand({
+    doc,
+    id: "Tools:" + id,
+    oncommand: 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");',
+  });
+
+  let key = null;
+  if (toolDefinition.key) {
+    key = doc.createElement("key");
+    key.id = "key_" + id;
+
+    if (toolDefinition.key.startsWith("VK_")) {
+      key.setAttribute("keycode", toolDefinition.key);
+    } else {
+      key.setAttribute("key", toolDefinition.key);
+    }
+
+    key.setAttribute("command", cmd.id);
+    key.setAttribute("modifiers", toolDefinition.modifiers);
+  }
+
+  let bc = createBroadcaster({
+    doc,
+    id: "devtoolsMenuBroadcaster_" + id,
+    label: toolDefinition.menuLabel || toolDefinition.label
+  });
+  bc.setAttribute("command", cmd.id);
+
+  if (key) {
+    bc.setAttribute("key", "key_" + id);
+  }
+
+  let menuitem = createMenuItem({
+    doc,
+    id: "menuitem_" + id,
+    broadcasterId: "devtoolsMenuBroadcaster_" + id,
+    accesskey: toolDefinition.accesskey
+  });
+
+  return {
+    cmd: cmd,
+    key: key,
+    bc: bc,
+    menuitem: menuitem
+  };
+}
+
+/**
+ * Create xul menuitem, command, broadcaster and key elements for a given tool.
+ * And then insert them into browser DOM.
+ *
+ * @param {XULDocument} doc
+ *        The document to which the tool is to be registered.
+ * @param {Object} toolDefinition
+ *        Tool definition of the tool to register.
+ * @param {Object} prevDef
+ *        The tool definition after which the tool menu item is to be added.
+ */
+function insertToolMenuElements(doc, toolDefinition, prevDef) {
+  let elements = createToolMenuElements(toolDefinition, doc);
+
+  doc.getElementById("mainCommandSet").appendChild(elements.cmd);
+
+  if (elements.key) {
+    attachKeybindingsToBrowser(doc, elements.key);
+  }
+
+  doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
+
+  let ref;
+  if (prevDef) {
+    let menuitem = doc.getElementById("menuitem_" + prevDef.id);
+    ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
+  } else {
+    ref = doc.getElementById("menu_devtools_separator");
+  }
+
+  if (ref) {
+    ref.parentNode.insertBefore(elements.menuitem, ref);
+  }
+}
+exports.insertToolMenuElements = insertToolMenuElements;
+
+/**
+ * Remove a tool's menuitem from a window
+ *
+ * @param {string} toolId
+ *        Id of the tool to add a menu entry for
+ * @param {XULDocument} doc
+ *        The document to which the tool menu item is to be removed from
+ */
+function removeToolFromMenu(toolId, doc) {
+  let command = doc.getElementById("Tools:" + toolId);
+  if (command) {
+    command.parentNode.removeChild(command);
+  }
+
+  let key = doc.getElementById("key_" + toolId);
+  if (key) {
+    key.parentNode.removeChild(key);
+  }
+
+  let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
+  if (bc) {
+    bc.parentNode.removeChild(bc);
+  }
+
+  let menuitem = doc.getElementById("menuitem_" + toolId);
+  if (menuitem) {
+    menuitem.parentNode.removeChild(menuitem);
+  }
+}
+exports.removeToolFromMenu = removeToolFromMenu;
+
+/**
+ * Add all tools to the developer tools menu of a window.
+ *
+ * @param {XULDocument} doc
+ *        The document to which the tool items are to be added.
+ */
+function addAllToolsToMenu(doc) {
+  let fragCommands = doc.createDocumentFragment();
+  let fragKeys = doc.createDocumentFragment();
+  let fragBroadcasters = doc.createDocumentFragment();
+  let fragMenuItems = doc.createDocumentFragment();
+
+  for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
+    if (!toolDefinition.inMenu) {
+      continue;
+    }
+
+    let elements = createToolMenuElements(toolDefinition, doc);
+
+    if (!elements) {
+      continue;
+    }
+
+    fragCommands.appendChild(elements.cmd);
+    if (elements.key) {
+      fragKeys.appendChild(elements.key);
+    }
+    fragBroadcasters.appendChild(elements.bc);
+    fragMenuItems.appendChild(elements.menuitem);
+  }
+
+  let mcs = doc.getElementById("mainCommandSet");
+  mcs.appendChild(fragCommands);
+
+  attachKeybindingsToBrowser(doc, fragKeys);
+
+  let mbs = doc.getElementById("mainBroadcasterSet");
+  mbs.appendChild(fragBroadcasters);
+
+  let mps = doc.getElementById("menu_devtools_separator");
+  if (mps) {
+    mps.parentNode.insertBefore(fragMenuItems, mps);
+  }
+}
+
+/**
+ * Add global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys and menus are to be added.
+ */
+function addTopLevelItems(doc) {
+  let keys = doc.createDocumentFragment();
+  let menuItems = doc.createDocumentFragment();
+
+  let { menuitems } = require("../menus");
+  for (let item of menuitems) {
+    if (item.separator) {
+      let separator = doc.createElement("menuseparator");
+      separator.id = item.id;
+      menuItems.appendChild(separator);
+    } else {
+      let { id, l10nKey } = item;
+
+      // Create a <menuitem>
+      let menuitem = createMenuItem({
+        doc,
+        id,
+        label: l10n(l10nKey + ".label"),
+        accesskey: l10n(l10nKey + ".accesskey"),
+        isCheckbox: item.checkbox
+      });
+      menuitem.addEventListener("command", item.oncommand);
+      menuItems.appendChild(menuitem);
+
+      if (item.key && l10nKey) {
+        // Create a <key>
+        let key = createKey(doc, l10nKey, null, item.key);
+        // Bug 371900: command event is fired only if "oncommand" attribute is set.
+        key.setAttribute("oncommand", ";");
+        key.addEventListener("command", item.oncommand);
+        // Refer to the key in order to display the key shortcut at menu ends
+        menuitem.setAttribute("key", key.id);
+        keys.appendChild(key);
+      }
+      if (item.additionalKeys) {
+        // Create additional <key>
+        for (let key of item.additionalKeys) {
+          let node = createKey(doc, key.l10nKey, null, key);
+          // Bug 371900: command event is fired only if "oncommand" attribute is set.
+          node.setAttribute("oncommand", ";");
+          node.addEventListener("command", item.oncommand);
+          keys.appendChild(node);
+        }
+      }
+    }
+  }
+
+  // Cache all nodes before insertion to be able to remove them on unload
+  let nodes = [];
+  for(let node of keys.children) {
+    nodes.push(node);
+  }
+  for(let node of menuItems.children) {
+    nodes.push(node);
+  }
+  FragmentsCache.set(doc, nodes);
+
+  attachKeybindingsToBrowser(doc, keys);
+
+  let menu = doc.getElementById("menuWebDeveloperPopup");
+  menu.appendChild(menuItems);
+
+  // There is still "Page Source" menuitem hardcoded into browser.xul. Instead
+  // of manually inserting everything around it, move it to the expected
+  // position.
+  let pageSource = doc.getElementById("menu_pageSource");
+  let endSeparator = doc.getElementById("devToolsEndSeparator");
+  menu.insertBefore(pageSource, endSeparator);
+}
+
+/**
+ * Remove global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys and menus are to be added.
+ */
+function removeTopLevelItems(doc) {
+  let nodes = FragmentsCache.get(doc);
+  if (!nodes) {
+    return;
+  }
+  FragmentsCache.delete(doc);
+  for (let node of nodes) {
+    node.remove();
+  }
+}
+
+/**
+ * Add menus and shortcuts to a browser document
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys and menus are to be added.
+ */
+exports.addMenus = function (doc) {
+  addTopLevelItems(doc);
+
+  addAllToolsToMenu(doc);
+};
+
+/**
+ * Remove menus and shortcuts from a browser document
+ *
+ * @param {XULDocument} doc
+ *        The document to which keys and menus are to be removed.
+ */
+exports.removeMenus = function (doc) {
+  // We only remove top level entries. Per-tool entries are removed while
+  // unregistering each tool.
+  removeTopLevelItems(doc);
+};
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -11,23 +11,25 @@
  * This module is loaded lazily by devtools-clhandler.js, once the first
  * browser window is ready (i.e. fired browser-delayed-startup-finished event)
  **/
 
 const {Cc, Ci, Cu} = require("chrome");
 const Services = require("Services");
 const promise = require("promise");
 const Telemetry = require("devtools/client/shared/telemetry");
-const {gDevTools} = require("./devtools");
+const { gDevTools } = require("./devtools");
+const { when: unload } = require("sdk/system/unload");
 
 // Load target and toolbox lazily as they need gDevTools to be fully initialized
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
 
 loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
 
 const bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 
 const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
 const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
 const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
@@ -73,64 +75,64 @@ var gDevToolsBrowser = exports.gDevTools
   /**
    * This function ensures the right commands are enabled in a window,
    * depending on their relevant prefs. It gets run when a window is registered,
    * or when any of the devtools prefs change.
    */
   updateCommandAvailability: function(win) {
     let doc = win.document;
 
-    function toggleCmd(id, isEnabled) {
+    function toggleMenuItem(id, isEnabled) {
       let cmd = doc.getElementById(id);
       if (isEnabled) {
         cmd.removeAttribute("disabled");
         cmd.removeAttribute("hidden");
       } else {
         cmd.setAttribute("disabled", "true");
         cmd.setAttribute("hidden", "true");
       }
     };
 
     // Enable developer toolbar?
     let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
-    toggleCmd("Tools:DevToolbar", devToolbarEnabled);
-    let focusEl = doc.getElementById("Tools:DevToolbarFocus");
+    toggleMenuItem("menu_devToolbar", devToolbarEnabled);
+    let focusEl = doc.getElementById("menu_devToolbar");
     if (devToolbarEnabled) {
       focusEl.removeAttribute("disabled");
     } else {
       focusEl.setAttribute("disabled", "true");
     }
     if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
       win.DeveloperToolbar.show(false).catch(console.error);
     }
 
     // Enable WebIDE?
     let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
-    toggleCmd("Tools:WebIDE", webIDEEnabled);
+    toggleMenuItem("menu_webide", webIDEEnabled);
 
     let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
     if (webIDEEnabled && showWebIDEWidget) {
       gDevToolsBrowser.installWebIDEWidget();
     } else {
       gDevToolsBrowser.uninstallWebIDEWidget();
     }
 
     // Enable Browser Toolbox?
     let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
     let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
     let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
-    toggleCmd("Tools:BrowserToolbox", remoteEnabled);
-    toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
+    toggleMenuItem("menu_browserToolbox", remoteEnabled);
+    toggleMenuItem("menu_browserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
 
     // Enable Error Console?
     let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
-    toggleCmd("Tools:ErrorConsole", consoleEnabled);
+    toggleMenuItem("javascriptConsole", consoleEnabled);
 
     // Enable DevTools connection screen, if the preference allows this.
-    toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled);
+    toggleMenuItem("menu_devtools_connect", devtoolsRemoteEnabled);
   },
 
   observe: function(subject, topic, prefName) {
     switch (topic) {
       case "browser-delayed-startup-finished":
         this._registerBrowserWindow(subject);
         break;
       case "nsPref:changed":
@@ -147,17 +149,16 @@ var gDevToolsBrowser = exports.gDevTools
 
   ensurePrefObserver: function() {
     if (!this._prefObserverRegistered) {
       this._prefObserverRegistered = true;
       Services.prefs.addObserver("devtools.", this, false);
     }
   },
 
-
   /**
    * This function is for the benefit of Tools:{toolId} commands,
    * triggered from the WebDeveloper menu and keyboard shortcuts.
    *
    * selectToolCommand's behavior:
    * - if the toolbox is closed,
    *   we open the toolbox and select the tool
    * - if the toolbox is open, and the targeted tool is not selected,
@@ -341,62 +342,38 @@ var gDevToolsBrowser = exports.gDevTools
   moveWebIDEWidgetInNavbar: function() {
     CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
   },
 
   /**
    * Add this DevTools's presence to a browser window's document
    *
    * @param {XULDocument} doc
-   *        The document to which menuitems and handlers are to be added
+   *        The document to which devtools should be hooked to.
    */
   _registerBrowserWindow: function(win) {
+    if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+      return;
+    }
+    gDevToolsBrowser._trackedBrowserWindows.add(win);
+
+    BrowserMenus.addMenus(win.document);
     this.updateCommandAvailability(win);
     this.ensurePrefObserver();
-    gDevToolsBrowser._trackedBrowserWindows.add(win);
     win.addEventListener("unload", this);
-    gDevToolsBrowser._addAllToolsToMenu(win.document);
-
-    if (this._isFirebugInstalled()) {
-      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
-      broadcaster.removeAttribute("key");
-    }
 
     let tabContainer = win.gBrowser.tabContainer;
     tabContainer.addEventListener("TabSelect", this, false);
     tabContainer.addEventListener("TabOpen", this, false);
     tabContainer.addEventListener("TabClose", this, false);
     tabContainer.addEventListener("TabPinned", this, false);
     tabContainer.addEventListener("TabUnpinned", this, false);
   },
 
   /**
-   * Add a <key> to <keyset id="devtoolsKeyset">.
-   * Appending a <key> element is not always enough. The <keyset> needs
-   * to be detached and reattached to make sure the <key> is taken into
-   * account (see bug 832984).
-   *
-   * @param {XULDocument} doc
-   *        The document to which keys are to be added
-   * @param {XULElement} or {DocumentFragment} keys
-   *        Keys to add
-   */
-  attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
-    let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
-
-    if (!devtoolsKeyset) {
-      devtoolsKeyset = doc.createElement("keyset");
-      devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
-    }
-    devtoolsKeyset.appendChild(keys);
-    let mainKeyset = doc.getElementById("mainKeyset");
-    mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
-  },
-
-  /**
    * Hook the JS debugger tool to the "Debug Script" button of the slow script
    * dialog.
    */
   setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
     let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
                          .getService(Ci.nsISlowScriptDebug);
     let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
 
@@ -479,26 +456,16 @@ var gDevToolsBrowser = exports.gDevTools
    */
   unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
     let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
                          .getService(Ci.nsISlowScriptDebug);
     debugService.activationHandler = undefined;
   },
 
   /**
-   * Detect the presence of a Firebug.
-   *
-   * @return promise
-   */
-  _isFirebugInstalled: function DT_isFirebugInstalled() {
-    let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
-    return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
-  },
-
-  /**
    * Add the menuitem for a tool to all open browser windows.
    *
    * @param {object} toolDefinition
    *        properties of the tool to add
    */
   _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
     // No menu item or global shortcut is required for options panel.
     if (!toolDefinition.inMenu) {
@@ -523,180 +490,24 @@ var gDevToolsBrowser = exports.gDevTools
       }
       if (def === toolDefinition) {
         break;
       }
       prevDef = def;
     }
 
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
-      let doc = win.document;
-      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
-
-      doc.getElementById("mainCommandSet").appendChild(elements.cmd);
-
-      if (elements.key) {
-        this.attachKeybindingsToBrowser(doc, elements.key);
-      }
-
-      doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
-
-      let amp = doc.getElementById("appmenu_webDeveloper_popup");
-      if (amp) {
-        let ref;
-
-        if (prevDef != null) {
-          let menuitem = doc.getElementById("appmenuitem_" + prevDef.id);
-          ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
-        } else {
-          ref = doc.getElementById("appmenu_devtools_separator");
-        }
-
-        if (ref) {
-          amp.insertBefore(elements.appmenuitem, ref);
-        }
-      }
-
-      let ref;
-
-      if (prevDef) {
-        let menuitem = doc.getElementById("menuitem_" + prevDef.id);
-        ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
-      } else {
-        ref = doc.getElementById("menu_devtools_separator");
-      }
-
-      if (ref) {
-        ref.parentNode.insertBefore(elements.menuitem, ref);
-      }
+      BrowserMenus.insertToolMenuElements(win.document, toolDefinition, prevDef);
     }
 
     if (toolDefinition.id === "jsdebugger") {
       gDevToolsBrowser.setSlowScriptDebugHandler();
     }
   },
 
-  /**
-   * Add all tools to the developer tools menu of a window.
-   *
-   * @param {XULDocument} doc
-   *        The document to which the tool items are to be added.
-   */
-  _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
-    let fragCommands = doc.createDocumentFragment();
-    let fragKeys = doc.createDocumentFragment();
-    let fragBroadcasters = doc.createDocumentFragment();
-    let fragAppMenuItems = doc.createDocumentFragment();
-    let fragMenuItems = doc.createDocumentFragment();
-
-    for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
-      if (!toolDefinition.inMenu) {
-        continue;
-      }
-
-      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
-
-      if (!elements) {
-        return;
-      }
-
-      fragCommands.appendChild(elements.cmd);
-      if (elements.key) {
-        fragKeys.appendChild(elements.key);
-      }
-      fragBroadcasters.appendChild(elements.bc);
-      fragAppMenuItems.appendChild(elements.appmenuitem);
-      fragMenuItems.appendChild(elements.menuitem);
-    }
-
-    let mcs = doc.getElementById("mainCommandSet");
-    mcs.appendChild(fragCommands);
-
-    this.attachKeybindingsToBrowser(doc, fragKeys);
-
-    let mbs = doc.getElementById("mainBroadcasterSet");
-    mbs.appendChild(fragBroadcasters);
-
-    let amps = doc.getElementById("appmenu_devtools_separator");
-    if (amps) {
-      amps.parentNode.insertBefore(fragAppMenuItems, amps);
-    }
-
-    let mps = doc.getElementById("menu_devtools_separator");
-    if (mps) {
-      mps.parentNode.insertBefore(fragMenuItems, mps);
-    }
-  },
-
-  /**
-   * Add a menu entry for a tool definition
-   *
-   * @param {string} toolDefinition
-   *        Tool definition of the tool to add a menu entry.
-   * @param {XULDocument} doc
-   *        The document to which the tool menu item is to be added.
-   */
-  _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) {
-    let id = toolDefinition.id;
-
-    // Prevent multiple entries for the same tool.
-    if (doc.getElementById("Tools:" + id)) {
-      return;
-    }
-
-    let cmd = doc.createElement("command");
-    cmd.id = "Tools:" + id;
-    cmd.setAttribute("oncommand",
-        'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");');
-
-    let key = null;
-    if (toolDefinition.key) {
-      key = doc.createElement("key");
-      key.id = "key_" + id;
-
-      if (toolDefinition.key.startsWith("VK_")) {
-        key.setAttribute("keycode", toolDefinition.key);
-      } else {
-        key.setAttribute("key", toolDefinition.key);
-      }
-
-      key.setAttribute("command", cmd.id);
-      key.setAttribute("modifiers", toolDefinition.modifiers);
-    }
-
-    let bc = doc.createElement("broadcaster");
-    bc.id = "devtoolsMenuBroadcaster_" + id;
-    bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
-    bc.setAttribute("command", cmd.id);
-
-    if (key) {
-      bc.setAttribute("key", "key_" + id);
-    }
-
-    let appmenuitem = doc.createElement("menuitem");
-    appmenuitem.id = "appmenuitem_" + id;
-    appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
-
-    let menuitem = doc.createElement("menuitem");
-    menuitem.id = "menuitem_" + id;
-    menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
-
-    if (toolDefinition.accesskey) {
-      menuitem.setAttribute("accesskey", toolDefinition.accesskey);
-    }
-
-    return {
-      cmd: cmd,
-      key: key,
-      bc: bc,
-      appmenuitem: appmenuitem,
-      menuitem: menuitem
-    };
-  },
-
   hasToolboxOpened: function(win) {
     let tab = win.gBrowser.selectedTab;
     for (let [target, toolbox] of gDevTools._toolboxes) {
       if (target.tab == tab) {
         return true;
       }
     }
     return false;
@@ -706,87 +517,54 @@ var gDevToolsBrowser = exports.gDevTools
    * Update the "Toggle Tools" checkbox in the developer tools menu. This is
    * called when a toolbox is created or destroyed.
    */
   _updateMenuCheckbox: function DT_updateMenuCheckbox() {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
 
       let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
 
-      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+      let broadcaster = win.document.getElementById("menu_devToolbox");
       if (hasToolbox) {
         broadcaster.setAttribute("checked", "true");
       } else {
         broadcaster.removeAttribute("checked");
       }
     }
   },
 
   /**
    * Remove the menuitem for a tool to all open browser windows.
    *
    * @param {string} toolId
    *        id of the tool to remove
    */
   _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
-      gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
+      BrowserMenus.removeToolFromMenu(toolId, win.document);
     }
 
     if (toolId === "jsdebugger") {
       gDevToolsBrowser.unsetSlowScriptDebugHandler();
     }
   },
 
   /**
-   * Remove a tool's menuitem from a window
-   *
-   * @param {string} toolId
-   *        Id of the tool to add a menu entry for
-   * @param {XULDocument} doc
-   *        The document to which the tool menu item is to be removed from
-   */
-  _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) {
-    let command = doc.getElementById("Tools:" + toolId);
-    if (command) {
-      command.parentNode.removeChild(command);
-    }
-
-    let key = doc.getElementById("key_" + toolId);
-    if (key) {
-      key.parentNode.removeChild(key);
-    }
-
-    let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
-    if (bc) {
-      bc.parentNode.removeChild(bc);
-    }
-
-    let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
-    if (appmenuitem) {
-      appmenuitem.parentNode.removeChild(appmenuitem);
-    }
-
-    let menuitem = doc.getElementById("menuitem_" + toolId);
-    if (menuitem) {
-      menuitem.parentNode.removeChild(menuitem);
-    }
-  },
-
-  /**
    * Called on browser unload to remove menu entries, toolboxes and event
    * listeners from the closed browser window.
    *
    * @param  {XULWindow} win
    *         The window containing the menu entry
    */
   _forgetBrowserWindow: function(win) {
     gDevToolsBrowser._trackedBrowserWindows.delete(win);
     win.removeEventListener("unload", this);
 
+    BrowserMenus.removeMenus(win.document);
+
     // Destroy toolboxes for closed window
     for (let [target, toolbox] of gDevTools._toolboxes) {
       if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) {
         toolbox.destroy();
       }
     }
 
     let tabContainer = win.gBrowser.tabContainer;
@@ -859,16 +637,20 @@ var gDevToolsBrowser = exports.gDevTools
     gDevToolsBrowser._telemetry = null;
 
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
       gDevToolsBrowser._forgetBrowserWindow(win);
     }
   },
 }
 
+// Handle all already registered tools,
+gDevTools.getToolDefinitionArray()
+         .forEach(def => gDevToolsBrowser._addToolToWindows(def));
+// and the new ones.
 gDevTools.on("tool-registered", function(ev, toolId) {
   let toolDefinition = gDevTools._tools.get(toolId);
   gDevToolsBrowser._addToolToWindows(toolDefinition);
 });
 
 gDevTools.on("tool-unregistered", function(ev, toolId) {
   if (typeof toolId != "string") {
     toolId = toolId.id;
@@ -876,23 +658,23 @@ gDevTools.on("tool-unregistered", functi
   gDevToolsBrowser._removeToolFromWindows(toolId);
 });
 
 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
 
 Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
 Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished", false);
+
 // Fake end of browser window load event for all already opened windows
 // that is already fully loaded.
 let enumerator = Services.wm.getEnumerator("navigator:browser");
 while (enumerator.hasMoreElements()) {
   let win = enumerator.getNext();
   if (win.gBrowserInit && win.gBrowserInit.delayedStartupFinished) {
     gDevToolsBrowser._registerBrowserWindow(win);
   }
 }
 
-// Load the browser devtools main module as the loader's main module.
-// This is done precisely here as main.js ends up dispatching the
-// tool-registered events we are listening in this module.
-loader.main("devtools/client/main");
-
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+  gDevToolsBrowser.destroy();
+});
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -11,42 +11,46 @@ const promise = require("promise");
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 
 const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
   require("devtools/client/definitions");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {JsonView} = require("devtools/client/jsonview/main");
 const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
+const {when: unload} = require("sdk/system/unload");
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 this.DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._themes = new Map();    // Map<themeId, theme>
   this._toolboxes = new Map(); // Map<target, toolbox>
 
   // destroy() is an observer's handler so we need to preserve context.
   this.destroy = this.destroy.bind(this);
-  this._teardown = this._teardown.bind(this);
 
   // JSON Viewer for 'application/json' documents.
   JsonView.initialize();
 
   AboutDevTools.register();
 
   EventEmitter.decorate(this);
 
-  Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
   Services.obs.addObserver(this.destroy, "quit-application", false);
+
+  // This is important step in initialization codepath where we are going to
+  // start registering all default tools and themes: create menuitems, keys, emit
+  // related events.
+  this.registerDefaults();
 };
 
 DevTools.prototype = {
   registerDefaults() {
     // Ensure registering items in the sorted order (getDefault* functions
     // return sorted lists)
     this.getDefaultTools().forEach(definition => this.registerTool(definition));
     this.getDefaultThemes().forEach(definition => this.registerTheme(definition));
@@ -476,33 +480,38 @@ DevTools.prototype = {
     AboutDevTools.unregister();
   },
 
   /**
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
     Services.obs.removeObserver(this.destroy, "quit-application");
-    Services.obs.removeObserver(this._teardown, "devtools-unloaded");
 
     for (let [key, tool] of this.getToolDefinitionMap()) {
       this.unregisterTool(key, true);
     }
 
     JsonView.destroy();
 
+    gDevTools.unregisterDefaults();
+
     // Cleaning down the toolboxes: i.e.
     //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
     // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
   },
 
   /**
    * Iterator that yields each of the toolboxes.
    */
   *[Symbol.iterator]() {
     for (let toolbox of this._toolboxes) {
       yield toolbox;
     }
   }
 };
 
-exports.gDevTools = new DevTools();
+const gDevTools = exports.gDevTools = new DevTools();
 
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+  gDevTools._teardown();
+});
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -7,16 +7,17 @@
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
     'test/shared-redux-head.js',
 ]
 
 DevToolsModules(
     'about-devtools-toolbox.js',
     'attach-thread.js',
+    'browser-menus.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
     'selection.js',
     'sidebar.js',
     'source-location.js',
     'target-from-url.js',
     'target.js',
--- a/devtools/client/framework/test/browser_source-location-01.js
+++ b/devtools/client/framework/test/browser_source-location-01.js
@@ -1,11 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+  "TypeError: this.transport is null");
+
 /**
  * Tests the SourceMapController updates generated sources when source maps
  * are subsequently found. Also checks when no column is provided, and
  * when tagging an already source mapped location initially.
  */
 
 const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
 // Empty page
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -4,20 +4,19 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-// Require this module just to setup things like themes and tools
-// devtools-browser is special as it loads main module
-// To be cleaned up in bug 1247203.
-require("devtools/client/framework/devtools-browser");
+// Require this module to setup core modules
+loader.main("devtools/client/main");
+
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var Services = require("Services");
 var { DebuggerClient } = require("devtools/shared/client/main");
 var { PrefsHelper } = require("devtools/client/shared/prefs");
 var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
--- a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
+++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
@@ -21,49 +21,50 @@ add_task(function*() {
   yield selectNode("#matches", inspector);
 
   info("Checking the property itself");
   let container = getComputedViewPropertyView(view, "color").valueNode;
   checkColorCycling(container, view);
 
   info("Checking matched selectors");
   container = yield getComputedViewMatchedRules(view, "color");
-  checkColorCycling(container, view);
+  yield checkColorCycling(container, view);
 });
 
-function checkColorCycling(container, view) {
-  let swatch = container.querySelector(".computedview-colorswatch");
+function* checkColorCycling(container, view) {
   let valueNode = container.querySelector(".computedview-color");
   let win = view.styleWindow;
 
   // "Authored" (default; currently the computed value)
   is(valueNode.textContent, "rgb(255, 0, 0)",
                             "Color displayed as an RGB value.");
 
-  // Hex
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
-
-  // HSL
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "hsl(0, 100%, 50%)",
-                            "Color displayed as an HSL value.");
+  let tests = [{
+    value: "red",
+    comment: "Color displayed as a color name."
+  }, {
+    value: "#f00",
+    comment: "Color displayed as an authored value."
+  }, {
+    value: "hsl(0, 100%, 50%)",
+    comment: "Color displayed as an HSL value again."
+  }, {
+    value: "rgb(255, 0, 0)",
+    comment: "Color displayed as an RGB value again."
+  }];
 
-  // RGB
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "rgb(255, 0, 0)",
-                            "Color displayed as an RGB value.");
+  for (let test of tests) {
+    yield checkSwatchShiftClick(container, win, test.value, test.comment);
+  }
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+  let swatch = container.querySelector(".computedview-colorswatch");
+  let valueNode = container.querySelector(".computedview-color");
 
-  // Color name
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "red",
-                            "Color displayed as a color name.");
-
-  // Back to "Authored"
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "rgb(255, 0, 0)",
-                            "Color displayed as an RGB value.");
+  let onUnitChange = swatch.once("unit-change");
+  EventUtils.synthesizeMouseAtCenter(swatch, {
+    type: "mousedown",
+    shiftKey: true
+  }, win);
+  yield onUnitChange;
+  is(valueNode.textContent, expectedValue, comment);
 }
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -495,25 +495,27 @@ InspectorPanel.prototype = {
    */
   updating: function(name) {
     if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
       this.cancelUpdate();
     }
 
     if (!this._updateProgress) {
       // Start an update in progress.
-      var self = this;
+      let self = this;
       this._updateProgress = {
         node: this.selection.nodeFront,
         outstanding: new Set(),
         checkDone: function() {
           if (this !== self._updateProgress) {
             return;
           }
-          if (this.node !== self.selection.nodeFront) {
+          // Cancel update if there is no `selection` anymore.
+          // It can happen if the inspector panel is already destroyed.
+          if (!self.selection || (this.node !== self.selection.nodeFront)) {
             self.cancelUpdate();
             return;
           }
           if (this.outstanding.size !== 0) {
             return;
           }
 
           self._updateProgress = null;
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -279,17 +279,21 @@
               </html:div>
             </html:section>
           </html:div>
         </tabpanel>
 
         <tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
           <html:div id="layout-container">
             <html:p id="layout-header">
-              <html:span id="layout-element-size"></html:span><html:span id="layout-element-position"></html:span>
+              <html:span id="layout-element-size"></html:span>
+              <html:section id="layout-position-group">
+                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+                <html:span id="layout-element-position"></html:span>
+              </html:section>
             </html:p>
 
             <html:div id="layout-main">
               <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
               <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
                 <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
                 <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
                   <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -135,16 +135,17 @@ EditingSession.prototype = {
  * currently loaded in the toolbox
  * @param {Window} win The window containing the panel
  */
 function LayoutView(inspector, win) {
   this.inspector = inspector;
   this.doc = win.document;
   this.sizeLabel = this.doc.querySelector(".layout-size > span");
   this.sizeHeadingLabel = this.doc.getElementById("layout-element-size");
+  this._geometryEditorHighlighter = null;
 
   this.init();
 }
 
 LayoutView.prototype = {
   init: function() {
     this.update = this.update.bind(this);
 
@@ -152,16 +153,21 @@ LayoutView.prototype = {
     this.inspector.selection.on("new-node-front", this.onNewSelection);
 
     this.onNewNode = this.onNewNode.bind(this);
     this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
 
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
     this.inspector.sidebar.on("select", this.onSidebarSelect);
 
+    this.onPickerStarted = this.onPickerStarted.bind(this);
+    this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+    this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+    this.onWillNavigate = this.onWillNavigate.bind(this);
+
     this.initBoxModelHighlighter();
 
     // Store for the different dimensions of the node.
     // 'selector' refers to the element that holds the value in view.xhtml;
     // 'property' is what we are measuring;
     // 'value' is the computed dimension, computed in update().
     this.map = {
       position: {
@@ -248,16 +254,21 @@ LayoutView.prototype = {
     this.onNewNode();
 
     // Mark document as RTL or LTR:
     let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
                     .getService(Ci.nsIXULChromeRegistry);
     let dir = chromeReg.isLocaleRTL("global");
     let container = this.doc.getElementById("layout-container");
     container.setAttribute("dir", dir ? "rtl" : "ltr");
+
+    let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+
+    this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
+    nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
   },
 
   initBoxModelHighlighter: function() {
     let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
     this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
     this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
 
     for (let element of highlightElts) {
@@ -371,19 +382,33 @@ LayoutView.prototype = {
   destroy: function() {
     let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
 
     for (let element of highlightElts) {
       element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
       element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
     }
 
+    let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+    nodeGeometry.removeEventListener("click", this.onGeometryButtonClick);
+
+    this.inspector.off("picker-started", this.onPickerStarted);
+
+    // Inspector Panel will destroy `markup` object on "will-navigate" event,
+    // therefore we have to check if it's still available in case LayoutView
+    // is destroyed immediately after.
+    if (this.inspector.markup) {
+      this.inspector.markup.off("leave", this.onMarkupViewLeave);
+      this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover);
+    }
+
     this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
     this.inspector.selection.off("new-node-front", this.onNewSelection);
     this.inspector.sidebar.off("select", this.onSidebarSelect);
+    this.inspector._target.off("will-navigate", this.onWillNavigate);
 
     this.sizeHeadingLabel = null;
     this.sizeLabel = null;
     this.inspector = null;
     this.doc = null;
 
     if (this.reflowFront) {
       this.untrackReflows();
@@ -396,20 +421,22 @@ LayoutView.prototype = {
     this.setActive(sidebar === "layoutview");
   },
 
   /**
    * Selection 'new-node-front' event handler.
    */
   onNewSelection: function() {
     let done = this.inspector.updating("layoutview");
-    this.onNewNode().then(done, err => {
-      console.error(err);
-      done();
-    });
+    this.onNewNode()
+      .then(() => this.hideGeometryEditor())
+      .then(done, (err) => {
+        console.error(err);
+        done();
+      }).catch(console.error);
   },
 
   /**
    * @return a promise that resolves when the view has been updated
    */
   onNewNode: function() {
     this.setActive(this.isViewVisibleAndNodeValid());
     return this.update();
@@ -427,16 +454,43 @@ LayoutView.prototype = {
       onlyRegionArea: true
     });
   },
 
   onHighlightMouseOut: function() {
     this.hideBoxModel();
   },
 
+  onGeometryButtonClick: function({target}) {
+    if (target.hasAttribute("checked")) {
+      target.removeAttribute("checked");
+      this.hideGeometryEditor();
+    } else {
+      target.setAttribute("checked", "true");
+      this.showGeometryEditor();
+    }
+  },
+
+  onPickerStarted: function() {
+    this.hideGeometryEditor();
+  },
+
+  onMarkupViewLeave: function() {
+    this.showGeometryEditor(true);
+  },
+
+  onMarkupViewNodeHover: function() {
+    this.hideGeometryEditor(false);
+  },
+
+  onWillNavigate: function() {
+    this._geometryEditorHighlighter.release().catch(console.error);
+    this._geometryEditorHighlighter = null;
+  },
+
   /**
    * Stop tracking reflows and hide all values when no node is selected or the
    * layout-view is hidden, otherwise track reflows and show values.
    * @param {Boolean} isActive
    */
   setActive: function(isActive) {
     if (isActive === this.isActive) {
       return;
@@ -454,27 +508,29 @@ LayoutView.prototype = {
   },
 
   /**
    * Compute the dimensions of the node and update the values in
    * the layoutview/view.xhtml document.
    * @return a promise that will be resolved when complete.
    */
   update: function() {
-    let lastRequest = Task.spawn((function*() {
+    let lastRequest = Task.spawn((function* () {
       if (!this.isViewVisibleAndNodeValid()) {
         return null;
       }
 
       let node = this.inspector.selection.nodeFront;
       let layout = yield this.inspector.pageStyle.getLayout(node, {
         autoMargins: this.isActive
       });
       let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
 
+      yield this.updateGeometryButton();
+
       // If a subsequent request has been made, wait for that one instead.
       if (this._lastRequest != lastRequest) {
         return this._lastRequest;
       }
 
       this._lastRequest = null;
       let width = layout.width;
       let height = layout.height;
@@ -543,17 +599,17 @@ LayoutView.prototype = {
       let newValue = width + "\u00D7" + height;
       if (this.sizeLabel.textContent != newValue) {
         this.sizeLabel.textContent = newValue;
       }
 
       this.elementRules = styleEntries.map(e => e.rule);
 
       this.inspector.emit("layoutview-updated");
-    }).bind(this)).then(null, console.error);
+    }).bind(this)).catch(console.error);
 
     this._lastRequest = lastRequest;
     return this._lastRequest;
   },
 
   /**
    * Update the text in the tooltip shown when hovering over a value to provide
    * information about the source CSS rule that sets this value.
@@ -603,16 +659,87 @@ LayoutView.prototype = {
    * Hide the box-model highlighter on the currently selected element
    */
   hideBoxModel: function() {
     let toolbox = this.inspector.toolbox;
 
     toolbox.highlighterUtils.unhighlight();
   },
 
+  /**
+   * Show the geometry editor highlighter on the currently selected element
+   * @param {Boolean} [showOnlyIfActive=false]
+   *   Indicates if the Geometry Editor should be shown only if it's active but
+   *   hidden.
+   */
+  showGeometryEditor: function(showOnlyIfActive = false) {
+    let toolbox = this.inspector.toolbox;
+    let nodeFront = this.inspector.selection.nodeFront;
+    let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+    let isActive = nodeGeometry.hasAttribute("checked");
+
+    if (showOnlyIfActive && !isActive) {
+      return;
+    }
+
+    if (this._geometryEditorHighlighter) {
+      this._geometryEditorHighlighter.show(nodeFront).catch(console.error);
+      return;
+    }
+
+    // instantiate Geometry Editor highlighter
+    toolbox.highlighterUtils
+      .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => {
+        highlighter.show(nodeFront).catch(console.error);
+        this._geometryEditorHighlighter = highlighter;
+
+        // Hide completely the geometry editor if the picker is clicked
+        toolbox.on("picker-started", this.onPickerStarted);
+
+        // Temporary hide the geometry editor
+        this.inspector.markup.on("leave", this.onMarkupViewLeave);
+        this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover);
+
+        // Release the actor on will-navigate event
+        this.inspector._target.once("will-navigate", this.onWillNavigate);
+      });
+  },
+
+  /**
+   * Hide the geometry editor highlighter on the currently selected element
+   * @param {Boolean} [updateButton=true]
+   *   Indicates if the Geometry Editor's button needs to be unchecked too
+   */
+  hideGeometryEditor: function(updateButton = true) {
+    if (this._geometryEditorHighlighter) {
+      this._geometryEditorHighlighter.hide().catch(console.error);
+    }
+
+    if (updateButton) {
+      let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+      nodeGeometry.removeAttribute("checked");
+    }
+  },
+
+  /**
+   * Update the visibility and the state of the geometry editor button,
+   * based on the selected node.
+   */
+  updateGeometryButton: Task.async(function* () {
+    let node = this.inspector.selection.nodeFront;
+    let isEditable = false;
+
+    if (node) {
+      isEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+    }
+
+    let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+    nodeGeometry.style.visibility = isEditable ? "visible" : "hidden";
+  }),
+
   manageOverflowingText: function(span) {
     let classList = span.parentNode.classList;
 
     if (classList.contains("layout-left") ||
         classList.contains("layout-right")) {
       let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
       classList.toggle("layout-rotate", force);
     }
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -204,16 +204,18 @@ MarkupView.prototype = {
     if (this._hoveredNode !== container.node) {
       if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
         this._showBoxModel(container.node);
       } else {
         this._hideBoxModel();
       }
     }
     this._showContainerAsHovered(container.node);
+
+    this.emit("node-hover");
   },
 
   /**
    * Executed on each mouse-move while a node is being dragged in the view.
    * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
    * node in.
    */
   _autoScroll: function(event) {
@@ -336,16 +338,18 @@ MarkupView.prototype = {
       return;
     }
 
     this._hideBoxModel(true);
     if (this._hoveredNode) {
       this.getContainer(this._hoveredNode).hovered = false;
     }
     this._hoveredNode = null;
+
+    this.emit("leave");
   },
 
   /**
    * Show the box model highlighter on a given node front
    *
    * @param  {NodeFront} nodeFront
    *         The node to show the highlighter for
    * @return {Promise} Resolves when the highlighter for this nodeFront is
@@ -2509,16 +2513,21 @@ function TextEditor(container, node, tem
 
   this.markup.template(template, this);
 
   editableField({
     element: this.value,
     stopOnReturn: true,
     trigger: "dblclick",
     multiline: true,
+    maxWidth: () => {
+      let elementRect = this.value.getBoundingClientRect();
+      let containerRect = this.container.elt.getBoundingClientRect();
+      return containerRect.right - elementRect.left - 2;
+    },
     trimOutput: false,
     done: (val, commit) => {
       if (!commit) {
         return;
       }
       this.node.getNodeValue().then(longstr => {
         longstr.string().then(oldValue => {
           longstr.release().then(null, console.error);
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -457,17 +457,18 @@ CssRuleView.prototype = {
    *
    * @param {DOMNode} target
    *        DOMNode target of the copy action
    */
   copySelection: function(target) {
     try {
       let text = "";
 
-      if (target && target.nodeName === "input") {
+      let nodeName = target && target.nodeName;
+      if (nodeName === "input" || nodeName == "textarea") {
         let start = Math.min(target.selectionStart, target.selectionEnd);
         let end = Math.max(target.selectionStart, target.selectionEnd);
         let count = end - start;
         text = target.value.substr(start, count);
       } else {
         text = this.styleWindow.getSelection().toString();
 
         // Remove any double newlines.
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -77,16 +77,17 @@ skip-if = e10s && debug # Bug 1250058 - 
 [browser_rules_context-menu-show-mdn-docs-02.js]
 [browser_rules_context-menu-show-mdn-docs-03.js]
 [browser_rules_copy_styles.js]
 [browser_rules_cssom.js]
 [browser_rules_cubicbezier-appears-on-swatch-click.js]
 [browser_rules_cubicbezier-commit-on-ENTER.js]
 [browser_rules_cubicbezier-revert-on-ESC.js]
 [browser_rules_custom.js]
+[browser_rules_cycle-angle.js]
 [browser_rules_cycle-color.js]
 [browser_rules_edit-property-cancel.js]
 [browser_rules_edit-property-click.js]
 [browser_rules_edit-property-commit.js]
 [browser_rules_edit-property-computed.js]
 [browser_rules_edit-property-increments.js]
 [browser_rules_edit-property-order.js]
 [browser_rules_edit-property-remove_01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js
@@ -0,0 +1,93 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test cycling angle units in the rule view.
+
+const TEST_URI = `
+  <style type="text/css">
+    body {
+      image-orientation: 1turn;
+    }
+    div {
+      image-orientation: 180deg;
+    }
+  </style>
+  <body><div>Test</div>cycling angle units in the rule view!</body>
+`;
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let container = getRuleViewProperty(
+    view, "body", "image-orientation").valueSpan;
+  yield checkAngleCycling(container, view);
+  yield checkAngleCyclingPersist(inspector, view);
+});
+
+function* checkAngleCycling(container, view) {
+  let valueNode = container.querySelector(".ruleview-angle");
+  let win = view.styleWindow;
+
+  // turn
+  is(valueNode.textContent, "1turn", "Angle displayed as a turn value.");
+
+  let tests = [{
+    value: "360deg",
+    comment: "Angle displayed as a degree value."
+  }, {
+    value: `${Math.round(Math.PI * 2 * 10000) / 10000}rad`,
+    comment: "Angle displayed as a radian value."
+  }, {
+    value: "400grad",
+    comment: "Angle displayed as a gradian value."
+  }, {
+    value: "1turn",
+    comment: "Angle displayed as a turn value again."
+  }];
+
+  for (let test of tests) {
+    yield checkSwatchShiftClick(container, win, test.value, test.comment);
+  }
+}
+
+function* checkAngleCyclingPersist(inspector, view) {
+  yield selectNode("div", inspector);
+  let container = getRuleViewProperty(
+    view, "div", "image-orientation").valueSpan;
+  let valueNode = container.querySelector(".ruleview-angle");
+  let win = view.styleWindow;
+
+  is(valueNode.textContent, "180deg", "Angle displayed as a degree value.");
+
+  yield checkSwatchShiftClick(container, win,
+    `${Math.round(Math.PI * 10000) / 10000}rad`,
+    "Angle displayed as a radian value.");
+
+  // Select the body and reselect the div to see
+  // if the new angle unit persisted
+  yield selectNode("body", inspector);
+  yield selectNode("div", inspector);
+
+  // We have to query for the container and the swatch because
+  // they've been re-generated
+  container = getRuleViewProperty(view, "div", "image-orientation").valueSpan;
+  valueNode = container.querySelector(".ruleview-angle");
+  is(valueNode.textContent, `${Math.round(Math.PI * 10000) / 10000}rad`,
+    "Angle still displayed as a radian value.");
+}
+
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+  let swatch = container.querySelector(".ruleview-angleswatch");
+  let valueNode = container.querySelector(".ruleview-angle");
+
+  let onUnitChange = swatch.once("unit-change");
+  EventUtils.synthesizeMouseAtCenter(swatch, {
+    type: "mousedown",
+    shiftKey: true
+  }, win);
+  yield onUnitChange;
+  is(valueNode.textContent, expectedValue, comment);
+}
--- a/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
+++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js
@@ -6,58 +6,88 @@
 
 // Test cycling color types in the rule view.
 
 const TEST_URI = `
   <style type="text/css">
     body {
       color: #f00;
     }
+    span {
+      color: blue;
+    }
   </style>
-  Test cycling color types in the rule view!
+  <body><span>Test</span> cycling color types in the rule view!</body>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
-  let {view} = yield openRuleView();
+  let {inspector, view} = yield openRuleView();
   let container = getRuleViewProperty(view, "body", "color").valueSpan;
-  checkColorCycling(container, view);
+  yield checkColorCycling(container, view);
+  yield checkColorCyclingPersist(inspector, view);
 });
 
-function checkColorCycling(container, view) {
-  let swatch = container.querySelector(".ruleview-colorswatch");
+function* checkColorCycling(container, view) {
   let valueNode = container.querySelector(".ruleview-color");
   let win = view.styleWindow;
 
   // Hex
   is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
 
-  // HSL
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "hsl(0, 100%, 50%)",
-                            "Color displayed as an HSL value.");
+  let tests = [{
+    value: "hsl(0, 100%, 50%)",
+    comment: "Color displayed as an HSL value."
+  }, {
+    value: "rgb(255, 0, 0)",
+    comment: "Color displayed as an RGB value."
+  }, {
+    value: "red",
+    comment: "Color displayed as a color name."
+  }, {
+    value: "#f00",
+    comment: "Color displayed as an authored value."
+  }, {
+    value: "hsl(0, 100%, 50%)",
+    comment: "Color displayed as an HSL value again."
+  }];
 
-  // RGB
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "rgb(255, 0, 0)",
-                            "Color displayed as an RGB value.");
+  for (let test of tests) {
+    yield checkSwatchShiftClick(container, win, test.value, test.comment);
+  }
+}
+
+function* checkColorCyclingPersist(inspector, view) {
+  yield selectNode("span", inspector);
+  let container = getRuleViewProperty(view, "span", "color").valueSpan;
+  let valueNode = container.querySelector(".ruleview-color");
+  let win = view.styleWindow;
 
-  // Color name
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "red",
-                            "Color displayed as a color name.");
+  is(valueNode.textContent, "blue", "Color displayed as a color name.");
+
+  yield checkSwatchShiftClick(container, win, "#00f",
+    "Color displayed as a hex value.");
+
+  // Select the body and reselect the span to see
+  // if the new color unit persisted
+  yield selectNode("body", inspector);
+  yield selectNode("span", inspector);
 
-  // "Authored"
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "#f00",
-                            "Color displayed as an authored value.");
+  // We have to query for the container and the swatch because
+  // they've been re-generated
+  container = getRuleViewProperty(view, "span", "color").valueSpan;
+  valueNode = container.querySelector(".ruleview-color");
+  is(valueNode.textContent, "#00f",
+    "Color  is still displayed as a hex value.");
+}
 
-  // One more click skips hex, because it is the same as authored, and
-  // instead goes back to HSL.
-  EventUtils.synthesizeMouseAtCenter(swatch,
-                                     {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "hsl(0, 100%, 50%)",
-                            "Color displayed as an HSL value again.");
+function* checkSwatchShiftClick(container, win, expectedValue, comment) {
+  let swatch = container.querySelector(".ruleview-colorswatch");
+  let valueNode = container.querySelector(".ruleview-color");
+
+  let onUnitChange = swatch.once("unit-change");
+  EventUtils.synthesizeMouseAtCenter(swatch, {
+    type: "mousedown",
+    shiftKey: true
+  }, win);
+  yield onUnitChange;
+  is(valueNode.textContent, expectedValue, comment);
 }
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js
@@ -20,35 +20,27 @@ add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode("#testid", inspector);
 
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let propEditor = ruleEditor.rule.textProps[0].editor;
 
   yield focusEditableField(view, propEditor.nameSpan);
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element,
-    ["VK_DELETE", "VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+    ["DELETE", "ESCAPE"]);
 
   is(propEditor.nameSpan.textContent, "background-color",
     "'background-color' property name is correctly set.");
   is((yield getComputedStyleProperty("#testid", null, "background-color")),
     "rgb(0, 0, 255)", "#00F background color is set.");
 
   yield focusEditableField(view, propEditor.valueSpan);
   let onValueDeleted = view.once("ruleview-changed");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element,
-    ["VK_DELETE", "VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+    ["DELETE", "ESCAPE"]);
   yield onValueDeleted;
 
   is(propEditor.valueSpan.textContent, "#00F",
     "'#00F' property value is correctly set.");
   is((yield getComputedStyleProperty("#testid", null, "background-color")),
     "rgb(0, 0, 255)", "#00F background color is set.");
 });
-
-function* sendCharsAndWaitForFocus(view, element, chars) {
-  let onFocus = once(element, "focus", true);
-  for (let ch of chars) {
-    EventUtils.sendChar(ch, view.styleWindow);
-  }
-  yield onFocus;
-}
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js
@@ -25,45 +25,37 @@ add_task(function*() {
 
 function* testEditPropertyAndCancel(inspector, view) {
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let propEditor = ruleEditor.rule.textProps[0].editor;
 
   info("Test editor is created when clicking on property name");
   yield focusEditableField(view, propEditor.nameSpan);
   ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
 
   info("Test editor is created when clicking on ':' next to property name");
   let nameRect = propEditor.nameSpan.getBoundingClientRect();
   yield focusEditableField(view, propEditor.nameSpan, nameRect.width + 1);
   ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
 
   info("Test editor is created when clicking on property value");
   yield focusEditableField(view, propEditor.valueSpan);
   ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
   // When cancelling a value edition, the text-property-editor will trigger
   // a modification to make sure the property is back to its original value
   // => need to wait on "ruleview-changed" to avoid unhandled promises
   let onRuleviewChanged = view.once("ruleview-changed");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
   yield onRuleviewChanged;
 
   info("Test editor is created when clicking on ';' next to property value");
   let valueRect = propEditor.valueSpan.getBoundingClientRect();
   yield focusEditableField(view, propEditor.valueSpan, valueRect.width + 1);
   ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value");
   // When cancelling a value edition, the text-property-editor will trigger
   // a modification to make sure the property is back to its original value
   // => need to wait on "ruleview-changed" to avoid unhandled promises
   onRuleviewChanged = view.once("ruleview-changed");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_ESCAPE"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]);
   yield onRuleviewChanged;
 }
-
-function* sendCharsAndWaitForFocus(view, element, chars) {
-  let onFocus = once(element, "focus", true);
-  for (let ch of chars) {
-    EventUtils.sendChar(ch, view.styleWindow);
-  }
-  yield onFocus;
-}
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js
@@ -29,30 +29,22 @@ add_task(function*() {
 
   let ruleEditor = getRuleViewRuleEditor(view, 1);
   let propEditor = ruleEditor.rule.textProps[1].editor;
 
   yield focusEditableField(view, propEditor.valueSpan);
 
   info("Deleting all the text out of a value field");
   let onRuleViewChanged = view.once("ruleview-changed");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element,
-    ["VK_DELETE", "VK_RETURN"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element,
+    ["DELETE", "RETURN"]);
   yield onRuleViewChanged;
 
   info("Pressing enter a couple times to cycle through editors");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
   onRuleViewChanged = view.once("ruleview-changed");
-  yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]);
+  yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]);
   yield onRuleViewChanged;
 
   isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none",
     "The name span is visible");
   is(ruleEditor.rule.textProps.length, 2, "Correct number of props");
 });
-
-function* sendCharsAndWaitForFocus(view, element, chars) {
-  let onFocus = once(element, "focus", true);
-  for (let ch of chars) {
-    EventUtils.sendChar(ch, element.ownerDocument.defaultView);
-  }
-  yield onFocus;
-}
--- a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
@@ -27,17 +27,18 @@ add_task(function*() {
   is(ruleEditor.rule.textProps.length, 2,
     "Should have created a new text property.");
   is(ruleEditor.propertyList.children.length, 2,
     "Should have created a property editor.");
 
   // Value is focused, lets add multiple rules here and make sure they get added
   onMutation = inspector.once("markupmutation");
   onRuleViewChanged = view.once("ruleview-changed");
-  let valueEditor = ruleEditor.propertyList.children[1].querySelector("input");
+  let valueEditor = ruleEditor.propertyList.children[1]
+    .querySelector(".styleinspector-propertyeditor");
   valueEditor.value = "10px;background:orangered;color: black;";
   EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
   yield onMutation;
   yield onRuleViewChanged;
 
   is(ruleEditor.rule.textProps.length, 4,
     "Should have added the changed value.");
   is(ruleEditor.propertyList.children.length, 5,
--- 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
@@ -39,16 +39,17 @@ const TEST_URI = `
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode("div", inspector);
   yield checkCopySelection(view);
   yield checkSelectAll(view);
+  yield checkCopyEditorValue(view);
 });
 
 function* checkCopySelection(view) {
   info("Testing selection copy");
 
   let contentDoc = view.styleDocument;
   let win = view.styleWindow;
   let prop = contentDoc.querySelector(".ruleview-property");
@@ -70,17 +71,17 @@ function* checkCopySelection(view) {
                         "    color: #000000;[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
   yield onPopup;
 
   ok(!view._contextmenu.menuitemCopy.hidden,
-    "Copy menu item is not hidden as expected");
+    "Copy menu item is displayed as expected");
 
   try {
     yield waitForClipboard(() => view._contextmenu.menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
   }
 
@@ -108,28 +109,66 @@ function* checkSelectAll(view) {
                         "}[\\r\\n]*";
 
   let onPopup = once(view._contextmenu._menupopup, "popupshown");
   EventUtils.synthesizeMouseAtCenter(prop,
     {button: 2, type: "contextmenu"}, win);
   yield onPopup;
 
   ok(!view._contextmenu.menuitemCopy.hidden,
-    "Copy menu item is not hidden as expected");
+    "Copy menu item is displayed as expected");
 
   try {
     yield waitForClipboard(() => view._contextmenu.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;
+
+  ok(!view._contextmenu.menuitemCopy.hidden,
+    "Copy menu item is displayed as expected");
+
+  try {
+    yield waitForClipboard(() => view._contextmenu.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);
 }
 
 function failedClipboard(expectedPattern) {
   // Format expected text for comparison
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -872,8 +872,29 @@ function* reloadPage(inspector, testActo
  */
 function* addNewRule(inspector, view) {
   info("Adding the new rule using the button");
   view.addRuleButton.click();
 
   info("Waiting for rule view to change");
   yield view.once("ruleview-changed");
 }
+
+/**
+ * Simulate a sequence of non-character keys (return, escape, tab) and wait for
+ * a given element to receive the focus.
+ *
+ * @param {CssRuleView} view
+ *        The instance of the rule-view panel
+ * @param {DOMNode} element
+ *        The element that should be focused
+ * @param {Array} keys
+ *        Array of non-character keys, the part that comes after "DOM_VK_" eg.
+ *        "RETURN", "ESCAPE"
+ * @return a promise that resolves after the element received the focus
+ */
+function* sendKeysAndWaitForFocus(view, element, keys) {
+  let onFocus = once(element, "focus", true);
+  for (let key of keys) {
+    EventUtils.sendKey(key, view.styleWindow);
+  }
+  yield onFocus;
+}
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -228,17 +228,19 @@ TextPropertyEditor.prototype = {
         start: this._onStartEditing,
         element: this.valueSpan,
         done: this._onValueDone,
         destroy: this.update,
         validate: this._onValidate,
         advanceChars: advanceValidate,
         contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
         property: this.prop,
-        popup: this.popup
+        popup: this.popup,
+        multiline: true,
+        maxWidth: () => this.container.getBoundingClientRect().width
       });
     }
   },
 
   /**
    * Get the path from which to resolve requests for this
    * rule's stylesheet.
    *
@@ -365,16 +367,17 @@ TextPropertyEditor.prototype = {
         // Adding this swatch to the list of swatches our colorpicker
         // knows about
         this.ruleView.tooltips.colorPicker.addSwatch(span, {
           onShow: this._onStartEditing,
           onPreview: this._onSwatchPreview,
           onCommit: this._onSwatchCommit,
           onRevert: this._onSwatchRevert
         });
+        span.on("unit-change", this._onSwatchCommit);
       }
     }
 
     // Attach the cubic-bezier tooltip to the bezier swatches
     this._bezierSwatchSpans =
       this.valueSpan.querySelectorAll("." + bezierSwatchClass);
     if (this.ruleEditor.isEditable) {
       for (let span of this._bezierSwatchSpans) {
@@ -399,16 +402,24 @@ TextPropertyEditor.prototype = {
           onShow: this._onStartEditing,
           onPreview: this._onSwatchPreview,
           onCommit: this._onSwatchCommit,
           onRevert: this._onSwatchRevert
         }, outputParser, parserOptions);
       }
     }
 
+    this.angleSwatchSpans =
+      this.valueSpan.querySelectorAll("." + angleSwatchClass);
+    if (this.ruleEditor.isEditable) {
+      for (let angleSpan of this.angleSwatchSpans) {
+        angleSpan.on("unit-change", this._onSwatchCommit);
+      }
+    }
+
     // Populate the computed styles.
     this._updateComputed();
 
     // Update the rule property highlight.
     this.ruleView._updatePropertyHighlight(this);
   },
 
   _onStartEditing: function() {
@@ -606,16 +617,23 @@ TextPropertyEditor.prototype = {
    *
    * @param {Number} direction
    *        The move focus direction number.
    */
   remove: function(direction) {
     if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
       for (let span of this._colorSwatchSpans) {
         this.ruleView.tooltips.colorPicker.removeSwatch(span);
+        span.off("unit-change", this._onSwatchCommit);
+      }
+    }
+
+    if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+      for (let span of this.angleSwatchSpans) {
+        span.off("unit-change", this._onSwatchCommit);
       }
     }
 
     this.element.parentNode.removeChild(this.element);
     this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
     this.nameSpan.textProperty = null;
     this.valueSpan.textProperty = null;
     this.prop.remove();
--- a/devtools/client/inspector/shared/style-inspector-menu.js
+++ b/devtools/client/inspector/shared/style-inspector-menu.js
@@ -271,18 +271,17 @@ StyleInspectorMenu.prototype = {
     }
   },
 
   _hasTextSelected: function() {
     let hasTextSelected;
     let selection = this.styleWindow.getSelection();
 
     let node = this._getClickedNode();
-    if (node.nodeName == "input") {
-       // input type="text"
+    if (node.nodeName == "input" || node.nodeName == "textarea") {
       let { selectionStart, selectionEnd } = node;
       hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd)
         && selectionStart !== selectionEnd;
     } else {
       hasTextSelected = selection.toString() && !selection.isCollapsed;
     }
 
     return hasTextSelected;
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -52,16 +52,17 @@ support-files =
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-geometry_01.js]
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
+[browser_inspector_highlighter-geometry_06.js]
 [browser_inspector_highlighter-hover_01.js]
 [browser_inspector_highlighter-hover_02.js]
 [browser_inspector_highlighter-hover_03.js]
 [browser_inspector_highlighter-iframes_01.js]
 [browser_inspector_highlighter-iframes_02.js]
 [browser_inspector_highlighter-inline.js]
 [browser_inspector_highlighter-keybinding_01.js]
 [browser_inspector_highlighter-keybinding_02.js]
--- a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js
@@ -1,95 +1,89 @@
 /* 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";
 
 // Test the creation of the geometry highlighter elements.
 
-const TEST_URL = "data:text/html;charset=utf-8," +
-                 "<span id='inline'></span>" +
-                 "<div id='positioned' style='background:yellow;position:absolute;left:5rem;top:30px;right:300px;bottom:10em;'></div>" +
-                 "<div id='sized' style='background:red;width:5em;height:50%;'></div>";
+const TEST_URL = `data:text/html;charset=utf-8,
+                  <span id='inline'></span>
+                  <div id='positioned' style='
+                    background:yellow;
+                    position:absolute;
+                    left:5rem;
+                    top:30px;
+                    right:300px;
+                    bottom:10em;'></div>
+                  <div id='sized' style='
+                    background:red;
+                    width:5em;
+                    height:50%;'></div>`;
+
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
 const ID = "geometry-editor-";
 const SIDES = ["left", "right", "top", "bottom"];
-const SIZES = ["width", "height"];
 
-add_task(function*() {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
-  let front = inspector.inspector;
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URL)
+                       .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+  let { finalize } = helper;
 
-  let highlighter = yield front.getHighlighterByType("GeometryEditorHighlighter");
+  helper.prefix = ID;
 
-  yield hasArrowsAndLabels(highlighter, inspector, testActor);
-  yield isHiddenForNonPositionedNonSizedElement(highlighter, inspector, testActor);
-  yield sideArrowsAreDisplayedForPositionedNode(highlighter, inspector, testActor);
-  yield sizeLabelIsDisplayedForSizedNode(highlighter, inspector, testActor);
+  yield hasArrowsAndLabelsAndHandlers(helper);
+  yield isHiddenForNonPositionedNonSizedElement(helper);
+  yield sideArrowsAreDisplayedForPositionedNode(helper);
 
-  yield highlighter.finalize();
+  finalize();
 });
 
-function* hasArrowsAndLabels(highlighterFront, inspector, testActor) {
+function* hasArrowsAndLabelsAndHandlers({getElementAttribute}) {
   info("Checking that the highlighter has the expected arrows and labels");
 
   for (let name of [...SIDES]) {
-    let value = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "class", highlighterFront);
+    let value = yield getElementAttribute("arrow-" + name, "class");
     is(value, ID + "arrow " + name, "The " + name + " arrow exists");
 
-    value = yield testActor.getHighlighterNodeAttribute(ID + "label-text-" + name, "class", highlighterFront);
+    value = yield getElementAttribute("label-text-" + name, "class");
     is(value, ID + "label-text", "The " + name + " label exists");
-  }
-
-  let value = yield testActor.getHighlighterNodeAttribute(ID + "label-text-size", "class", highlighterFront);
-  is(value, ID + "label-text", "The size label exists");
-}
-
-function* isHiddenForNonPositionedNonSizedElement(highlighterFront, inspector, testActor) {
-  info("Asking to show the highlighter on an inline, non positioned element");
 
-  let node = yield getNodeFront("#inline", inspector);
-  yield highlighterFront.show(node);
-
-  for (let name of [...SIDES]) {
-    let hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "hidden", highlighterFront);
-    is(hidden, "true", "The " + name + " arrow is hidden");
+    value = yield getElementAttribute("handler-" + name, "class");
+    is(value, ID + "handler-" + name, "The " + name + " handler exists");
   }
-
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "label-size", "hidden", highlighterFront);
-  is(hidden, "true", "The size label is hidden");
 }
 
-function* sideArrowsAreDisplayedForPositionedNode(highlighterFront, inspector, testActor) {
+function* isHiddenForNonPositionedNonSizedElement(
+  {show, hide, isElementHidden}) {
+  info("Asking to show the highlighter on an inline, non p  ositioned element");
+
+  yield show("#inline");
+
+  for (let name of [...SIDES]) {
+    let hidden = yield isElementHidden("arrow-" + name);
+    ok(hidden, "The " + name + " arrow is hidden");
+
+    hidden = yield isElementHidden("handler-" + name);
+    ok(hidden, "The " + name + " handler is hidden");
+  }
+}
+
+function* sideArrowsAreDisplayedForPositionedNode(
+  {show, hide, isElementHidden}) {
   info("Asking to show the highlighter on the positioned node");
 
-  let node = yield getNodeFront("#positioned", inspector);
-  yield highlighterFront.show(node);
+  yield show("#positioned");
 
   for (let name of SIDES) {
-    let hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "hidden", highlighterFront);
+    let hidden = yield isElementHidden("arrow-" + name);
     ok(!hidden, "The " + name + " arrow is visible for the positioned node");
-  }
-
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "label-size", "hidden", highlighterFront);
-  is(hidden, "true", "The size label is hidden");
-
-  info("Hiding the highlighter");
-  yield highlighterFront.hide();
-}
 
-function* sizeLabelIsDisplayedForSizedNode(highlighterFront, inspector, testActor) {
-  info("Asking to show the highlighter on the sized node");
-
-  let node = yield getNodeFront("#sized", inspector);
-  yield highlighterFront.show(node);
-
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "label-size", "hidden", highlighterFront);
-  ok(!hidden, "The size label is visible");
-
-  for (let name of SIDES) {
-    let hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "hidden", highlighterFront);
-    is(hidden, "true", "The " + name + " arrow is hidden for the sized node");
+    hidden = yield isElementHidden("handler-" + name);
+    ok(!hidden, "The " + name + " handler is visible for the positioned node");
   }
 
   info("Hiding the highlighter");
-  yield highlighterFront.hide();
+  yield hide();
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js
@@ -1,24 +1,48 @@
 /* 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/. */
 
+/* Globals defined in: devtools/client/inspector/test/head.js */
+
 "use strict";
 
 // Test that the geometry highlighter labels are correct.
 
-const TEST_URL = "data:text/html;charset=utf-8," +
-                 "<div id='positioned' style='background:yellow;position:absolute;left:5rem;top:30px;right:300px;bottom:10em;'></div>" +
-                 "<div id='positioned2' style='background:blue;position:absolute;right:10%;top:5vmin;'>test element</div>" +
-                 "<div id='relative' style='background:green;position:relative;top:10px;left:20px;bottom:30px;right:40px;width:100px;height:100px;'></div>" +
-                 "<div id='relative2' style='background:grey;position:relative;top:0;bottom:-50px;height:3em;'>relative</div>" +
-                 "<div id='sized' style='background:red;width:5em;height:50%;'></div>" +
-                 "<div id='sized2' style='background:orange;width:40px;position:absolute;right:0;bottom:0'>wow</div>";
+const TEST_URL = `data:text/html;charset=utf-8,
+                  <div id='positioned' style='
+                    background:yellow;
+                    position:absolute;
+                    left:5rem;
+                    top:30px;
+                    right:300px;
+                    bottom:10em;'></div>
+                  <div id='positioned2' style='
+                    background:blue;
+                    position:absolute;
+                    right:10%;
+                    top:5vmin;'>test element</div>
+                 <div id='relative' style='
+                    background:green;
+                    position:relative;
+                    top:10px;
+                    left:20px;
+                    bottom:30px;
+                    right:40px;
+                    width:100px;
+                    height:100px;'></div>
+                 <div id='relative2' style='
+                    background:grey;
+                    position:relative;
+                    top:0;bottom:-50px;
+                    height:3em;'>relative</div>`;
+
 const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
 
 const POSITIONED_ELEMENT_TESTS = [{
   selector: "#positioned",
   expectedLabels: [
     {side: "left", visible: true, label: "5rem"},
     {side: "top", visible: true, label: "30px"},
     {side: "right", visible: true, label: "300px"},
     {side: "bottom", visible: true, label: "10em"}
@@ -27,24 +51,16 @@ const POSITIONED_ELEMENT_TESTS = [{
   selector: "#positioned2",
   expectedLabels: [
     {side: "left", visible: false},
     {side: "top", visible: true, label: "5vmin"},
     {side: "right", visible: true, label: "10%"},
     {side: "bottom", visible: false}
   ]
 }, {
-  selector: "#sized",
-  expectedLabels: [
-    {side: "left", visible: false},
-    {side: "top", visible: false},
-    {side: "right", visible: false},
-    {side: "bottom", visible: false}
-  ]
-}, {
   selector: "#relative",
   expectedLabels: [
     {side: "left", visible: true, label: "20px"},
     {side: "top", visible: true, label: "10px"},
     {side: "right", visible: false},
     {side: "bottom", visible: false}
   ]
 }, {
@@ -52,91 +68,49 @@ const POSITIONED_ELEMENT_TESTS = [{
   expectedLabels: [
     {side: "left", visible: false},
     {side: "top", visible: true, label: "0px"},
     {side: "right", visible: false},
     {side: "bottom", visible: false}
   ]
 }];
 
-const SIZED_ELEMENT_TESTS = [{
-  selector: "#positioned",
-  visible: false
-}, {
-  selector: "#sized",
-  visible: true,
-  expected: "\u2194 5em \u2195 50%"
-}, {
-  selector: "#relative",
-  visible: true,
-  expected: "\u2194 100px \u2195 100px"
-}, {
-  selector: "#relative2",
-  visible: true,
-  expected: "\u2195 3em"
-}, {
-  selector: "#sized2",
-  visible: true,
-  expected: "\u2194 40px"
-}];
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URL)
+                       .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+  helper.prefix = ID;
 
-add_task(function*() {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
-  let front = inspector.inspector;
+  let { finalize } = helper;
 
-  let highlighter = yield front.getHighlighterByType("GeometryEditorHighlighter");
+  yield positionLabelsAreCorrect(helper);
 
-  yield positionLabelsAreCorrect(highlighter, inspector, testActor);
-  yield sizeLabelIsCorrect(highlighter, inspector, testActor);
-
-  yield highlighter.finalize();
+  yield finalize();
 });
 
-function* positionLabelsAreCorrect(highlighterFront, inspector, testActor) {
+function* positionLabelsAreCorrect(
+  {show, hide, isElementHidden, getElementTextContent}
+) {
   info("Highlight nodes and check position labels");
 
   for (let {selector, expectedLabels} of POSITIONED_ELEMENT_TESTS) {
     info("Testing node " + selector);
-    let node = yield getNodeFront(selector, inspector);
-    yield highlighterFront.show(node);
+
+    yield show(selector);
 
     for (let {side, visible, label} of expectedLabels) {
-      let id = ID + "label-" + side;
+      let id = "label-" + side;
 
-      let hidden = yield testActor.getHighlighterNodeAttribute(id, "hidden", highlighterFront);
+      let hidden = yield isElementHidden(id);
       if (visible) {
         ok(!hidden, "The " + side + " label is visible");
 
-        let value = yield testActor.getHighlighterNodeTextContent(id, highlighterFront);
+        let value = yield getElementTextContent(id);
         is(value, label, "The " + side + " label textcontent is correct");
       } else {
-        is(hidden, "true", "The " + side + " label is hidden");
+        ok(hidden, "The " + side + " label is hidden");
       }
     }
 
     info("Hiding the highlighter");
-    yield highlighterFront.hide();
+    yield hide();
   }
 }
-
-function* sizeLabelIsCorrect(highlighterFront, inspector, testActor) {
-  info("Highlight nodes and check size labels");
-
-  let id = ID + "label-size";
-  for (let {selector, visible, expected} of SIZED_ELEMENT_TESTS) {
-    info("Testing node " + selector);
-    let node = yield getNodeFront(selector, inspector);
-    yield highlighterFront.show(node);
-
-    let hidden = yield testActor.getHighlighterNodeAttribute(id, "hidden", highlighterFront);
-    if (!visible) {
-      is(hidden, "true", "The size label is hidden");
-    } else {
-      ok(!hidden, "The size label is visible");
-
-      let label = yield testActor.getHighlighterNodeTextContent(id, highlighterFront);
-      is(label, expected, "The size label textcontent is correct");
-    }
-
-    info("Hiding the highlighter");
-    yield highlighterFront.hide();
-  }
-}
--- a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js
@@ -1,60 +1,61 @@
 /* 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/. */
 
+/* Globals defined in: devtools/client/inspector/test/head.js */
+
 "use strict";
 
 // Test that the right arrows/labels are shown even when the css properties are
 // in several different css rules.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
 const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
 const PROPS = ["left", "right", "top", "bottom"];
 
-add_task(function*() {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
-  let front = inspector.inspector;
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URL)
+                       .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
 
-  let highlighter = yield front.getHighlighterByType("GeometryEditorHighlighter");
+  helper.prefix = ID;
+
+  let { finalize } = helper;
 
-  yield checkArrowsLabels("#node1", ["size"],
-                          highlighter, inspector, testActor);
+  yield checkArrowsLabelsAndHandlers(
+    "#node2", ["top", "left", "bottom", "right"],
+     helper);
 
-  yield checkArrowsLabels("#node2", ["top", "left", "bottom", "right"],
-                          highlighter, inspector, testActor);
+  yield checkArrowsLabelsAndHandlers("#node3", ["top", "left"], helper);
 
-  yield checkArrowsLabels("#node3", ["top", "left", "size"],
-                          highlighter, inspector, testActor);
-
-  yield highlighter.finalize();
+  yield finalize();
 });
 
-function* checkArrowsLabels(selector, expectedProperties, highlighterFront, inspector, testActor) {
+function* checkArrowsLabelsAndHandlers(selector, expectedProperties,
+  {show, hide, isElementHidden}
+) {
   info("Getting node " + selector + " from the page");
-  let node = yield getNodeFront(selector, inspector);
 
-  info("Highlighting the node");
-  yield highlighterFront.show(node);
+  yield show(selector);
 
   for (let name of expectedProperties) {
-    let hidden;
-    if (name === "size") {
-      hidden = yield testActor.getHighlighterNodeAttribute(ID + "label-size", "hidden", highlighterFront);
-    } else {
-      hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "hidden", highlighterFront);
-    }
-    ok(!hidden, "The " + name + " arrow/label is visible for node " + selector);
+    let hidden = (yield isElementHidden("arrow-" + name)) &&
+                 (yield isElementHidden("handler-" + name));
+    ok(!hidden,
+      "The " + name + " label/arrow & handler is visible for node " + selector);
   }
 
   // Testing that the other arrows are hidden
   for (let name of PROPS) {
     if (expectedProperties.indexOf(name) !== -1) {
       continue;
     }
-    let hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, "hidden", highlighterFront);
-    is(hidden, "true", "The " + name + " arrow is hidden for node " + selector);
+    let hidden = (yield isElementHidden("arrow-" + name)) &&
+                 (yield isElementHidden("handler-" + name));
+    ok(hidden,
+      "The " + name + " arrow & handler is hidden for node " + selector);
   }
 
   info("Hiding the highlighter");
-  yield highlighterFront.hide();
+  yield hide();
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js
@@ -1,56 +1,85 @@
 /* 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/. */
 
+ /* Globals defined in: devtools/client/inspector/test/head.js */
+
 "use strict";
 
-// Test that the arrows are positioned correctly and have the right size.
+// Test that the arrows and handlers are positioned correctly and have the right
+// size.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
 const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
 
-add_task(function*() {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
-  let front = inspector.inspector;
+const handlerMap = {
+  "top": {"cx": "x2", "cy": "y2"},
+  "bottom": {"cx": "x2", "cy": "y2"},
+  "left": {"cx": "x2", "cy": "y2"},
+  "right": {"cx": "x2", "cy": "y2"}
+};
 
-  let highlighter = yield front.getHighlighterByType("GeometryEditorHighlighter");
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URL)
+                       .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+
+  helper.prefix = ID;
 
-  yield checkArrows(highlighter, inspector, testActor, ".absolute-all-4", {
-   "top": {x1: 506, y1: 51, x2: 506, y2: 61},
-   "bottom": {x1: 506, y1: 451, x2: 506, y2: 251},
-   "left": {x1: 401, y1: 156, x2: 411, y2: 156},
-   "right": {x1: 901, y1: 156, x2: 601, y2: 156}
+  let { hide, finalize } = helper;
+
+  yield checkArrowsAndHandlers(helper, ".absolute-all-4", {
+    "top": {x1: 506, y1: 51, x2: 506, y2: 61},
+    "bottom": {x1: 506, y1: 451, x2: 506, y2: 251},
+    "left": {x1: 401, y1: 156, x2: 411, y2: 156},
+    "right": {x1: 901, y1: 156, x2: 601, y2: 156}
   });
 
-  yield checkArrows(highlighter, inspector, testActor, ".relative", {
-   "top": {x1: 901, y1: 51, x2: 901, y2: 91},
-   "left": {x1: 401, y1: 97, x2: 651, y2: 97}
+  yield checkArrowsAndHandlers(helper, ".relative", {
+    "top": {x1: 901, y1: 51, x2: 901, y2: 91},
+    "left": {x1: 401, y1: 97, x2: 651, y2: 97}
   });
 
-  yield checkArrows(highlighter, inspector, testActor, ".fixed", {
-   "top": {x1: 25, y1: 0, x2: 25, y2: 400},
-   "left": {x1: 0, y1: 425, x2: 0, y2: 425}
+  yield checkArrowsAndHandlers(helper, ".fixed", {
+    "top": {x1: 25, y1: 0, x2: 25, y2: 400},
+    "left": {x1: 0, y1: 425, x2: 0, y2: 425}
   });
 
   info("Hiding the highlighter");
-  yield highlighter.hide();
-  yield highlighter.finalize();
+  yield hide();
+  yield finalize();
 });
 
-function* checkArrows(highlighter, inspector, testActor, selector, arrows) {
+function* checkArrowsAndHandlers(helper, selector, arrows) {
   info("Highlighting the test node " + selector);
-  let node = yield getNodeFront(selector, inspector);
-  yield highlighter.show(node);
+
+  yield helper.show(selector);
 
   for (let side in arrows) {
-    yield checkArrow(highlighter, testActor, side, arrows[side]);
+    yield checkArrowAndHandler(helper, side, arrows[side]);
   }
 }
 
-function* checkArrow(highlighter, testActor, name, expectedCoordinates) {
-  for (let coordinate in expectedCoordinates) {
-    let value = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + name, coordinate, highlighter);
-    is(Math.floor(value), expectedCoordinates[coordinate],
+function* checkArrowAndHandler({getElementAttribute}, name, expectedCoords) {
+  info("Checking " + name + "arrow and handler coordinates are correct");
+
+  let handlerX = yield getElementAttribute("handler-" + name, "cx");
+  let handlerY = yield getElementAttribute("handler-" + name, "cy");
+
+  let expectedHandlerX = yield getElementAttribute("arrow-" + name,
+                                handlerMap[name].cx);
+  let expectedHandlerY = yield getElementAttribute("arrow-" + name,
+                                handlerMap[name].cy);
+
+  is(handlerX, expectedHandlerX,
+    "coordinate X for handler " + name + " is correct.");
+  is(handlerY, expectedHandlerY,
+    "coordinate Y for handler " + name + " is correct.");
+
+  for (let coordinate in expectedCoords) {
+    let value = yield getElementAttribute("arrow-" + name, coordinate);
+
+    is(Math.floor(value), expectedCoords[coordinate],
       coordinate + " coordinate for arrow " + name + " is correct");
   }
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js
@@ -1,134 +1,119 @@
 /* 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/. */
 
+ /* Globals defined in: devtools/client/inspector/test/head.js */
+
 "use strict";
 
-// Test that the arrows and offsetparent and currentnode elements of the
-// geometry highlighter only appear when needed.
+// Test that the arrows/handlers and offsetparent and currentnode elements of
+// the geometry highlighter only appear when needed.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_02.html";
 const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
 
 const TEST_DATA = [{
   selector: "body",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: false,
-  hasVisibleArrows: false,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: false
 }, {
   selector: "h1",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: false,
-  hasVisibleArrows: false,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: false
 }, {
   selector: ".absolute",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: true
 }, {
   selector: "#absolute-container",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: false,
-  isSizeVisible: true
+  hasVisibleArrowsAndHandlers: false
 }, {
   selector: ".absolute-bottom-right",
   isOffsetParentVisible: true,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: true
 }, {
   selector: ".absolute-width-margin",
   isOffsetParentVisible: true,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: true
+  hasVisibleArrowsAndHandlers: true
 }, {
   selector: ".absolute-all-4",
   isOffsetParentVisible: true,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: true
 }, {
   selector: ".relative",
   isOffsetParentVisible: true,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: true
 }, {
   selector: ".static",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: false,
-  hasVisibleArrows: false,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: false
 }, {
   selector: ".static-size",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: false,
-  isSizeVisible: true
+  hasVisibleArrowsAndHandlers: false
 }, {
   selector: ".fixed",
   isOffsetParentVisible: false,
   isCurrentNodeVisible: true,
-  hasVisibleArrows: true,
-  isSizeVisible: false
+  hasVisibleArrowsAndHandlers: true
 }];
 
-add_task(function*() {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
-  let front = inspector.inspector;
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URL)
+                       .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
 
-  let highlighter = yield front.getHighlighterByType("GeometryEditorHighlighter");
+  helper.prefix = ID;
+
+  let { hide, finalize } = helper;
 
   for (let data of TEST_DATA) {
-    yield testNode(inspector, highlighter, testActor, data);
+    yield testNode(helper, data);
   }
 
   info("Hiding the highlighter");
-  yield highlighter.hide();
-  yield highlighter.finalize();
+  yield hide();
+  yield finalize();
 });
 
-function* testNode(inspector, highlighter, testActor, data) {
-  info("Highlighting the test node " + data.selector);
-  let node = yield getNodeFront(data.selector, inspector);
-  yield highlighter.show(node);
+function* testNode(helper, data) {
+  let { selector } = data;
+  yield helper.show(data.selector);
 
-  is((yield isOffsetParentVisible(highlighter, testActor)), data.isOffsetParentVisible,
-    "The offset-parent highlighter visibility is correct for node " + data.selector);
-  is((yield isCurrentNodeVisible(highlighter, testActor)), data.isCurrentNodeVisible,
-    "The current-node highlighter visibility is correct for node " + data.selector);
-  is((yield hasVisibleArrows(highlighter, testActor)), data.hasVisibleArrows,
-    "The arrows visibility is correct for node " + data.selector);
-  is((yield isSizeVisible(highlighter, testActor)), data.isSizeVisible,
-    "The size label visibility is correct for node " + data.selector);
+  is((yield isOffsetParentVisible(helper)), data.isOffsetParentVisible,
+    "The offset-parent highlighter visibility is correct for node " + selector);
+  is((yield isCurrentNodeVisible(helper)), data.isCurrentNodeVisible,
+    "The current-node highlighter visibility is correct for node " + selector);
+  is((yield hasVisibleArrowsAndHandlers(helper)),
+    data.hasVisibleArrowsAndHandlers,
+    "The arrows visibility is correct for node " + selector);
 }
 
-function* isOffsetParentVisible(highlighter, testActor) {
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "offset-parent", "hidden", highlighter);
-  return !hidden;
+function* isOffsetParentVisible({isElementHidden}) {
+  return !(yield isElementHidden("offset-parent"));
 }
 
-function* isCurrentNodeVisible(highlighter, testActor) {
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "current-node", "hidden", highlighter);
-  return !hidden;
+function* isCurrentNodeVisible({isElementHidden}) {
+  return !(yield isElementHidden("current-node"));
 }
 
-function* hasVisibleArrows(highlighter, testActor) {
+function* hasVisibleArrowsAndHandlers({isElementHidden}) {
   for (let side of ["top", "left", "bottom", "right"]) {
-    let hidden = yield testActor.getHighlighterNodeAttribute(ID + "arrow-" + side, "hidden", highlighter);
+    let hidden = yield isElementHidden("arrow-" + side);
     if (!hidden) {
-      return true;
+      return !(yield isElementHidden("handler-" + side));
     }
   }
   return false;
 }
-
-function* isSizeVisible(highlighter, testActor) {
-  let hidden = yield testActor.getHighlighterNodeAttribute(ID + "label-size", "hidden", highlighter);
-  return !hidden;
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js
@@ -0,0 +1,166 @@
+/* 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";
+
+// Test that the geometry editor resizes properly an element on all sides,
+// with different unit measures, and that arrow/handlers are updated correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const SIDES = ["top", "right", "bottom", "left"];
+
+// The object below contains all the tests for this unit test.
+// The property's name is the test's description, that points to an
+// object contains the steps (what side of the geometry editor to drag,
+// the amount of pixels) and the expectation.
+const TESTS = {
+  "Drag top's handler along x and y, south-east direction": {
+    "expects": "Only y axis is used to updated the top's element value",
+    "drag": "top",
+    "by": {x: 10, y: 10}
+  },
+  "Drag right's handler along x and y, south-east direction": {
+    "expects": "Only x axis is used to updated the right's element value",
+    "drag": "right",
+    "by": {x: 10, y: 10}
+  },
+  "Drag bottom's handler along x and y, south-east direction": {
+    "expects": "Only y axis is used to updated the bottom's element value",
+    "drag": "bottom",
+    "by": {x: 10, y: 10}
+  },
+  "Drag left's handler along x and y, south-east direction": {
+    "expects": "Only y axis is used to updated the left's element value",
+    "drag": "left",
+    "by": {x: 10, y: 10}
+  },
+  "Drag top's handler along x and y, north-west direction": {
+    "expects": "Only y axis is used to updated the top's element value",
+    "drag": "top",
+    "by": {x: -20, y: -20}
+  },
+  "Drag right's handler along x and y, north-west direction": {
+    "expects": "Only x axis is used to updated the right's element value",
+    "drag": "right",
+    "by": {x: -20, y: -20}
+  },
+  "Drag bottom's handler along x and y, north-west direction": {
+    "expects": "Only y axis is used to updated the bottom's element value",
+    "drag": "bottom",
+    "by": {x: -20, y: -20}
+  },
+  "Drag left's handler along x and y, north-west direction": {
+    "expects": "Only y axis is used to updated the left's element value",
+    "drag": "left",
+    "by": {x: -20, y: -20}
+  }
+};
+
+add_task(function* () {
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+
+  helper.prefix = ID;
+
+  let { show, hide, finalize } = helper;
+
+  info("Showing the highlighter");
+  yield show("#node2");
+
+  for (let desc in TESTS) {
+    yield executeTest(helper, desc, TESTS[desc]);
+  }
+
+  info("Hiding the highlighter");
+  yield hide();
+  yield finalize();
+});
+
+function* executeTest(helper, desc, data) {
+  info(desc);
+
+  ok((yield areElementAndHighlighterMovedCorrectly(
+    helper, data.drag, data.by)), data.expects);
+}
+
+function* areElementAndHighlighterMovedCorrectly(helper, side, by) {
+  let { mouse, reflow, highlightedNode } = helper;
+
+  let {x, y} = yield getHandlerCoords(helper, side);
+
+  let dx = x + by.x;
+  let dy = y + by.y;
+
+  let beforeDragStyle = yield highlightedNode.getComputedStyle();
+
+  // simulate drag & drop
+  yield mouse.down(x, y);
+  yield mouse.move(dx, dy);
+  yield mouse.up();
+
+  yield reflow();
+
+  info(`Checking ${side} handler is moved correctly`);
+  yield isHandlerPositionUpdated(helper, side, x, y, by);
+
+  let delta = (side === "left" || side === "right") ? by.x : by.y;
+  delta = delta * ((side === "right" || side === "bottom") ? -1 : 1);
+
+  info("Checking element's sides are correct after drag & drop");
+  return yield areElementSideValuesCorrect(highlightedNode, beforeDragStyle,
+                                           side, delta);
+}
+
+function* isHandlerPositionUpdated(helper, name, x, y, by) {
+  let {x: afterDragX, y: afterDragY} = yield getHandlerCoords(helper, name);
+
+  if (name === "left" || name === "right") {
+    is(afterDragX, x + by.x,
+      `${name} handler's x axis updated.`);
+    is(afterDragY, y,
+      `${name} handler's y axis unchanged.`);
+  } else {
+    is(afterDragX, x,
+      `${name} handler's x axis unchanged.`);
+    is(afterDragY, y + by.y,
+      `${name} handler's y axis updated.`);
+  }
+}
+
+function* areElementSideValuesCorrect(node, beforeDragStyle, name, delta) {
+  let afterDragStyle = yield node.getComputedStyle();
+  let isSideCorrect = true;
+
+  for (let side of SIDES) {
+    let afterValue = Math.round(parseFloat(afterDragStyle[side].value));
+    let beforeValue = Math.round(parseFloat(beforeDragStyle[side].value));
+
+    if (side === name) {
+      // `isSideCorrect` is used only as test's return value, not to perform
+      // the actual test, because with `is` instead of `ok` we gather more
+      // information in case of failure
+      isSideCorrect = isSideCorrect && (afterValue === beforeValue + delta);
+
+      is(afterValue, beforeValue + delta,
+        `${side} is updated.`);
+    } else {
+      isSideCorrect = isSideCorrect && (afterValue === beforeValue);
+
+      is(afterValue, beforeValue,
+        `${side} is unchaged.`);
+    }
+  }
+
+  return isSideCorrect;
+}
+
+function* getHandlerCoords({getElementAttribute}, side) {
+  return {
+    x: Math.round(yield getElementAttribute("handler-" + side, "cx")),
+    y: Math.round(yield getElementAttribute("handler-" + side, "cy"))
+  };
+}
--- a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html
@@ -23,17 +23,17 @@
       position: absolute;
       background: #f06;
     }
     .pos-top-left {
       top: 30px;
       left: 25%;
     }
     .pos-bottom-right {
-      bottom: 5em;
+      bottom: 10em;
       right: -10px;
     }
 
     .inline-positioned {
       background: yellow;
     }
 
     #absolute-container {
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -470,24 +470,46 @@ function* getNodeFrontForSelector(select
  */
 const getHighlighterHelperFor = (type) => Task.async(
   function*({inspector, testActor}) {
     let front = inspector.inspector;
     let highlighter = yield front.getHighlighterByType(type);
 
     let prefix = "";
 
+    // Internals for mouse events
+    let prevX, prevY;
+
+    // Highlighted node
+    let  highlightedNode = null;
+
     return {
       set prefix(value) {
         prefix = value;
       },
+      get highlightedNode() {
+        if (!highlightedNode) {
+          return null;
+        }
+
+        return {
+          getComputedStyle: function*(options = {}) {
+            return yield inspector.pageStyle.getComputed(
+              highlightedNode, options);
+          }
+        };
+      },
 
       show: function*(selector = ":root") {
-        let node = yield getNodeFront(selector, inspector);
-        yield highlighter.show(node);
+        highlightedNode = yield getNodeFront(selector, inspector);
+        return yield highlighter.show(highlightedNode);
+      },
+
+      hide: function*() {
+        yield highlighter.hide();
       },
 
       isElementHidden: function*(id) {
         return (yield testActor.getHighlighterNodeAttribute(
           prefix + id, "hidden", highlighter)) === "true";
       },
 
       getElementTextContent: function*(id) {
@@ -496,20 +518,45 @@ const getHighlighterHelperFor = (type) =
       },
 
       getElementAttribute: function*(id, name) {
         return yield testActor.getHighlighterNodeAttribute(
           prefix + id, name, highlighter);
       },
 
       synthesizeMouse: function*(options) {
+        options = Object.assign({selector: ":root"}, options);
         yield testActor.synthesizeMouse(options);
       },
 
+      // This object will synthesize any "mouse" prefixed event to the
+      // `testActor`, using the name of method called as suffix for the
+      // event's name.
+      // If no x, y coords are given, the previous ones are used.
+      //
+      // For example:
+      //   mouse.down(10, 20); // synthesize "mousedown" at 10,20
+      //   mouse.move(20, 30); // synthesize "mousemove" at 20,30
+      //   mouse.up();         // synthesize "mouseup" at 20,30
+      mouse: new Proxy({}, {
+        get: (target, name) =>
+          function*(x = prevX, y = prevY) {
+            prevX = x;
+            prevY = y;
+            yield testActor.synthesizeMouse({
+              selector: ":root", x, y, options: {type: "mouse" + name}});
+          }
+      }),
+
+      reflow: function*() {
+        yield testActor.reflow();
+      },
+
       finalize: function*() {
+        highlightedNode = null;
         yield highlighter.finalize();
       }
     };
   }
 );
 
 // The expand all operation of the markup-view calls itself recursively and
 // there's not one event we can wait for to know when it's done
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -217,16 +217,17 @@ devtools.jar:
     skin/images/itemToggle@2x.png (themes/images/itemToggle@2x.png)
     skin/images/itemArrow-dark-rtl.svg (themes/images/itemArrow-dark-rtl.svg)
     skin/images/itemArrow-dark-ltr.svg (themes/images/itemArrow-dark-ltr.svg)
     skin/images/itemArrow-rtl.svg (themes/images/itemArrow-rtl.svg)
     skin/images/itemArrow-ltr.svg (themes/images/itemArrow-ltr.svg)
     skin/images/noise.png (themes/images/noise.png)
     skin/images/dropmarker.svg (themes/images/dropmarker.svg)
     skin/layout.css (themes/layout.css)
+    skin/images/geometry-editor.svg (themes/images/geometry-editor.svg)
     skin/images/debugger-pause.png (themes/images/debugger-pause.png)
     skin/images/debugger-pause@2x.png (themes/images/debugger-pause@2x.png)
     skin/images/debugger-play.png (themes/images/debugger-play.png)
     skin/images/debugger-play@2x.png (themes/images/debugger-play@2x.png)
     skin/images/fast-forward.png (themes/images/fast-forward.png)
     skin/images/fast-forward@2x.png (themes/images/fast-forward@2x.png)
     skin/images/rewind.png (themes/images/rewind.png)
     skin/images/rewind@2x.png (themes/images/rewind@2x.png)
--- a/devtools/client/jsonview/css/tabs.css
+++ b/devtools/client/jsonview/css/tabs.css
@@ -72,18 +72,17 @@
 
 .theme-firebug .tabs .tabs-menu-item a {
   padding: 5px 8px 4px 8px;;
   font-weight: bold;
   color: #565656;
   border-radius: 4px 4px 0 0;
 }
 
-.theme-firebug .tabs .tabs-menu-item.is-active a,
-.theme-firebug .tabs .tabs-menu-item.is-active a:focus {
+.theme-firebug .tabs .tabs-menu-item.is-active a {
   background-color: rgb(247, 251, 254);
   border: 1px solid rgb(170, 188, 207);
   border-bottom-color: transparent;
 }
 
 .theme-firebug .tabs .tabs-menu-item:hover a {
   border: 1px solid #C8C8C8;
   border-bottom: 1px solid transparent;
--- a/devtools/client/locales/en-US/layoutview.dtd
+++ b/devtools/client/locales/en-US/layoutview.dtd
@@ -11,13 +11,18 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (*.tooltip): These tooltips are not regular tooltips.
   -  The text appears on the bottom right corner of the layout view when
   -  the corresponding box is hovered. -->
 
-<!ENTITY layoutViewTitle        "Box Model">
-<!ENTITY margin.tooltip         "margin">
-<!ENTITY border.tooltip         "border">
-<!ENTITY padding.tooltip        "padding">
-<!ENTITY content.tooltip        "content">
+<!ENTITY layoutViewTitle          "Box Model">
+<!ENTITY margin.tooltip           "margin">
+<!ENTITY border.tooltip           "border">
+<!ENTITY padding.tooltip          "padding">
+<!ENTITY content.tooltip          "content">
+
+<!-- LOCALIZATION NOTE: This label is displayed as a tooltip that appears when
+  -  hovering over the button that allows users to edit the position of an
+  -  element in the page. -->
+<!ENTITY geometry.button.tooltip  "Edit position">
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/menus.properties
@@ -0,0 +1,70 @@
+# 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/.
+
+devToolsCmd.key = VK_F12
+devToolsCmd.keytext = F12
+
+devtoolsServiceWorkers.label = Service Workers
+devtoolsServiceWorkers.accesskey = k
+
+devtoolsConnect.label = Connect…
+devtoolsConnect.accesskey = C
+
+errorConsoleCmd.label = Error Console
+errorConsoleCmd.accesskey = C
+
+browserConsoleCmd.label = Browser Console
+browserConsoleCmd.accesskey = B
+browserConsoleCmd.key = j
+
+responsiveDesignMode.label = Responsive Design Mode
+responsiveDesignMode.accesskey = R
+responsiveDesignMode.key = M
+
+eyedropper.label = Eyedropper
+eyedropper.accesskey = Y
+
+# LOCALIZATION NOTE (scratchpad.label): This menu item label appears
+# in the Tools menu. See bug 653093.
+# The Scratchpad is intended to provide a simple text editor for creating
+# and evaluating bits of JavaScript code for the purposes of function
+# prototyping, experimentation and convenient scripting.
+#
+# It's quite possible that you won't have a good analogue for the word
+# "Scratchpad" in your locale. You should feel free to find a close
+# approximation to it or choose a word (or words) that means
+# "simple discardable text editor".
+scratchpad.label = Scratchpad
+scratchpad.accesskey = s
+scratchpad.key = VK_F4
+scratchpad.keytext = F4
+
+# LOCALIZATION NOTE (browserToolboxMenu.label): This is the label for the
+# application menu item that opens the browser toolbox UI in the Tools menu.
+browserToolboxMenu.label = Browser Toolbox
+browserToolboxMenu.accesskey = e
+browserToolboxMenu.key = i
+
+# LOCALIZATION NOTE (browserContentToolboxMenu.label): This is the label for the
+# application menu item that opens the browser content toolbox UI in the Tools menu.
+# This toolbox allows to debug the chrome of the content process in multiprocess builds.
+browserContentToolboxMenu.label = Browser Content Toolbox
+browserContentToolboxMenu.accesskey = x
+
+devToolbarMenu.label = Developer Toolbar
+devToolbarMenu.accesskey = v
+devToolbarMenu.key = VK_F2
+devToolbarMenu.keytext = F2
+
+webide.label = WebIDE
+webide.accesskey = W
+webide.key = VK_F8
+webide.keytext = F8
+
+devToolboxMenuItem.label = Toggle Tools
+devToolboxMenuItem.accesskey = T
+devToolboxMenuItem.key = I
+
+getMoreDevtoolsCmd.label = Get More Tools
+getMoreDevtoolsCmd.accesskey = M
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -64,17 +64,17 @@ netmonitor.security.disabled=Disabled
 # LOCALIZATION NOTE (netmonitor.security.hostHeader):
 # This string is used as a header for section containing security information
 # related to the remote host. %S is replaced with the domain name of the remote
 # host. For example: Host example.com
 netmonitor.security.hostHeader=Host %S:
 
 # LOCALIZATION NOTE (netmonitor.security.notAvailable):
 # This string is used to indicate that a certain piece of information is not
-# available to be displayd. For example a certificate that has no organization
+# available to be displayed. For example a certificate that has no organization
 # defined:
 #   Organization: <Not Available>
 netmonitor.security.notAvailable=<Not Available>
 
 # LOCALIZATION NOTE (collapseDetailsPane): This is the tooltip for the button
 # that collapses the network details pane in the UI.
 collapseDetailsPane=Hide request details
 
@@ -253,8 +253,58 @@ charts.totalSeconds=Time: #1 second;Time
 
 # LOCALIZATION NOTE (charts.totalCached): This is the label displayed
 # in the performance analysis view for total cached responses.
 charts.totalCached=Cached responses: %S
 
 # LOCALIZATION NOTE (charts.totalCount): This is the label displayed
 # in the performance analysis view for total requests.
 charts.totalCount=Total requests: %S
+
+# LOCALIZATION NOTE (netRequest.headers): A label used for Headers tab
+# This tab displays list of HTTP headers
+netRequest.headers=Headers
+
+# LOCALIZATION NOTE (netRequest.response): A label used for Response tab
+# This tab displays HTTP response body
+netRequest.response=Response
+
+# LOCALIZATION NOTE (netRequest.rawData): A label used for a section
+# in Response tab. This section displays raw response body as it's
+# been received from the backend (debugger server)
+netRequest.rawData=Raw Data
+
+# LOCALIZATION NOTE (netRequest.xml): A label used for a section
+# in Response tab. This section displays parsed XML response body.
+netRequest.xml=XML
+
+# LOCALIZATION NOTE (netRequest.image): A label used for a section
+# in Response tab. This section displays images returned in response body.
+netRequest.image=Image
+
+# LOCALIZATION NOTE (netRequest.sizeLimitMessage): A label used
+# in Response and Post tabs in case the body is bigger than given limit.
+# It allows the user to click and fetch more from the backend.
+# The {{link}} will be replace at run-time by an active link.
+# String with ID 'netRequest.sizeLimitMessageLink' will be used as text
+# for this link.
+netRequest.sizeLimitMessage=Size limit has been reached. Click {{link}} to load more.
+netRequest.sizeLimitMessageLink=here
+
+# LOCALIZATION NOTE (netRequest.responseBodyDiscarded): A label used
+# in Response tab if the response body is not available.
+netRequest.responseBodyDiscarded=Response body was not stored.
+
+# LOCALIZATION NOTE (netRequest.requestBodyDiscarded): A label used
+# in Post tab if the post body is not available.
+netRequest.requestBodyDiscarded=Request POST body was not stored.
+
+# LOCALIZATION NOTE (netRequest.post): A label used for Post tab
+# This tab displays HTTP post body
+netRequest.post=POST
+
+# LOCALIZATION NOTE (netRequest.cookies): A label used for Cookies tab
+# This tab displays request and response cookies.
+netRequest.cookies=Cookies
+
+# LOCALIZATION NOTE (netRequest.params): A label used for URL parameters tab
+# This tab displays data parsed from URL query string.
+netRequest.params=Params
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -34,16 +34,26 @@ toolboxToggleButton.warnings=#1 warning;
 
 # LOCALIZATION NOTE (toolboxToggleButton.tooltip): This string is shown
 # as tooltip in the developer toolbar to open/close the developer tools.
 # It's using toolboxToggleButton.errors as first and
 # toolboxToggleButton.warnings as second argument to show the number of errors
 # and warnings.
 toolboxToggleButton.tooltip=%1$S, %2$S\nClick to toggle the developer tools.
 
+# LOCALIZATION NOTE (toolbar.closeButton.tooltip)
+# Used as a message in tooltip when overing the close button of the Developer
+# Toolbar.
+toolbar.closeButton.tooltip=Close Developer Toolbar
+
+# LOCALIZATION NOTE (toolbar.toolsButton.tooltip)
+# Used as a message in tooltip when overing the wrench icon of the Developer
+# Toolbar, which toggle the developer toolbox.
+toolbar.toolsButton.tooltip=Toggle developer tools
+
 # LOCALIZATION NOTE (toolbox.titleTemplate): This is the template
 # used to format the title of the toolbox.
 # The name of the selected tool: %1$S.
 # The url of the page being tooled: %2$S.
 toolbox.titleTemplate=%1$S - %2$S
 
 # LOCALIZATION NOTE (toolbox.defaultTitle): This is used as the tool
 # name when no tool is selected.
@@ -110,9 +120,9 @@ toolbox.noContentProcess.message=No cont
 toolbox.viewCssSourceInStyleEditor.label=Open File in Style-Editor
 
 # LOCALIZATION NOTE (toolbox.viewJsSourceInDebugger.label)
 # Used as a message in either tooltips or contextual menu items to open the
 # corresponding URL as a js file in the Debugger tool.
 # DEV NOTE: Mostly used wherever toolbox.viewSourceInDebugger is used.
 toolbox.viewJsSourceInDebugger.label=Open File in Debugger
 
-toolbox.resumeOrderWarning=Page did not resume after the debugger was attached. To fix this, please close and re-open the toolbox.
\ No newline at end of file
+toolbox.resumeOrderWarning=Page did not resume after the debugger was attached. To fix this, please close and re-open the toolbox.
--- a/devtools/client/main.js
+++ b/devtools/client/main.js
@@ -1,30 +1,22 @@
 /* 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 Services = require("Services");
-const { gDevTools } = require("devtools/client/framework/devtools");
+/**
+ * This module could have been devtools-browser.js.
+ * But we need this wrapper in order to define precisely what we are exporting
+ * out of client module loader (Loader.jsm): only Toolbox and TargetFactory.
+ */
 
-// This is important step in initialization codepath where we are going to
-// start registering all default tools and themes: create menuitems, keys, emit
-// related events.
-gDevTools.registerDefaults();
-
+// For compatiblity reasons, exposes these symbols on "devtools":
 Object.defineProperty(exports, "Toolbox", {
   get: () => require("devtools/client/framework/toolbox").Toolbox
 });
 Object.defineProperty(exports, "TargetFactory", {
   get: () => require("devtools/client/framework/target").TargetFactory
 });
 
-const unloadObserver = {
-  observe: function(subject) {
-    if (subject.wrappedJSObject === require("@loader/unload")) {
-      Services.obs.removeObserver(unloadObserver, "sdk:loader:destroy");
-      gDevTools.unregisterDefaults();
-    }
-  }
-};
-Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false);
+// Load the main browser module
+require("devtools/client/framework/devtools-browser");
new file mode 100644
--- /dev/null
+++ b/devtools/client/menus.js
@@ -0,0 +1,207 @@
+/* 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";
+
+/**
+ * This module defines the sorted list of menuitems inserted into the
+ * "Web Developer" menu.
+ * It also defines the key shortcuts that relates to them.
+ *
+ * Various fields are necessary for historical compatiblity with XUL/addons:
+ * - id:
+ *   used as <xul:menuitem> id attribute
+ * - l10nKey:
+ *   prefix used to locale localization strings from menus.properties
+ * - oncommand:
+ *   function called when the menu item or key shortcut are fired
+ * - key:
+ *    - id:
+ *      prefixed by 'key_' to compute <xul:key> id attribute
+ *    - modifiers:
+ *      optional modifiers for the key shortcut
+ *    - keytext:
+ *      boolean, to set to true for key shortcut using regular character
+ * - additionalKeys:
+ *   Array of additional keys, see `key` definition.
+ * - disabled:
+ *   If true, the menuitem and key shortcut are going to be hidden and disabled
+ *   on startup, until some runtime code eventually enable them.
+ * - checkbox:
+ *   If true, the menuitem is prefixed by a checkbox and runtime code can
+ *   toggle it.
+ */
+
+const Services = require("Services");
+const isMac = Services.appinfo.OS === "Darwin";
+
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "Eyedropper", "devtools/client/eyedropper/eyedropper", true);
+
+loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
+loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
+loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+
+/**
+ * Detect the presence of a Firebug.
+ */
+function isFirebugInstalled() {
+  let bootstrappedAddons = Services.prefs
+    .getCharPref("extensions.bootstrappedAddons");
+  return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
+}
+
+exports.menuitems = [
+  { id: "menu_devToolbox",
+    l10nKey: "devToolboxMenuItem",
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+    },
+    key: {
+      id: "devToolboxMenuItem",
+      modifiers: isMac ? "accel,alt" : "accel,shift",
+      // This is the only one with a letter key
+      // and needs to be translated differently
+      keytext: true,
+    },
+    // This key conflicts with firebug, only enable it when it's not installed.
+    additionalKeys: !isFirebugInstalled() ? [{
+      id: "devToolboxMenuItemF12",
+      l10nKey: "devToolsCmd",
+    }] : null,
+    checkbox: true
+  },
+  { id: "menu_devtools_separator",
+    separator: true },
+  { id: "menu_devToolbar",
+    l10nKey: "devToolbarMenu",
+    disabled: true,
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      // Distinguish events when selecting a menuitem, where we either open
+      // or close the toolbar and when hitting the key shortcut where we just
+      // focus the toolbar if it doesn't already has it.
+      if (event.target.tagName.toLowerCase() == "menuitem") {
+        window.DeveloperToolbar.toggle();
+      } else {
+        window.DeveloperToolbar.focusToggle();
+      }
+    },
+    key: {
+      id: "devToolbar",
+      modifiers: "shift"
+    },
+    checkbox: true
+  },
+  { id: "menu_webide",
+    l10nKey: "webide",
+    disabled: true,
+    oncommand() {
+      gDevToolsBrowser.openWebIDE();
+    },
+    key: {
+      id: "webide",
+      modifiers: "shift"
+    }
+  },
+  { id: "menu_browserToolbox",
+    l10nKey: "browserToolboxMenu",
+    disabled: true,
+    oncommand() {
+      BrowserToolboxProcess.init();
+    },
+    key: {
+      id: "browserToolbox",
+      modifiers: "accel,alt,shift",
+      keytext: true
+    }
+  },
+  { id: "menu_browserContentToolbox",
+    l10nKey: "browserContentToolboxMenu",
+    disabled: true,
+    oncommand() {
+      gDevToolsBrowser.openContentProcessToolbox();
+    }
+  },
+  { id: "menu_browserConsole",
+    l10nKey: "browserConsoleCmd",
+    oncommand() {
+      let HUDService = require("devtools/client/webconsole/hudservice");
+      HUDService.openBrowserConsoleOrFocus();
+    },
+    key: {
+      id: "browserConsole",
+      modifiers: "accel,shift",
+      keytext: true
+    }
+  },
+  { id: "menu_responsiveUI",
+    l10nKey: "responsiveDesignMode",
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      ResponsiveUIManager.toggle(window, window.gBrowser.selectedTab);
+    },
+    key: {
+      id: "responsiveUI",
+      modifiers: isMac ? "accel,alt" : "accel,shift",
+      keytext: true
+    },
+    checkbox: true
+  },
+  { id: "menu_eyedropper",
+    l10nKey: "eyedropper",
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      let eyedropper = new Eyedropper(window, { context: "menu",
+                                                copyOnSelect: true });
+      eyedropper.open();
+    },
+    checkbox: true
+  },
+  { id: "menu_scratchpad",
+    l10nKey: "scratchpad",
+    oncommand() {
+      ScratchpadManager.openScratchpad();
+    },
+    key: {
+      id: "scratchpad",
+      modifiers: "shift"
+    }
+  },
+  { id: "javascriptConsole",
+    l10nKey: "errorConsoleCmd",
+    disabled: true,
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      window.toJavaScriptConsole();
+    }
+  },
+  { id: "menu_devtools_serviceworkers",
+    l10nKey: "devtoolsServiceWorkers",
+    disabled: true,
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      gDevToolsBrowser.openAboutDebugging(window.gBrowser, "workers");
+    }
+  },
+  { id: "menu_devtools_connect",
+    l10nKey: "devtoolsConnect",
+    disabled: true,
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      gDevToolsBrowser.openConnectScreen(window.gBrowser);
+    }
+  },
+  { separator: true,
+    id: "devToolsEndSeparator"
+  },
+  { id: "getMoreDevtools",
+    l10nKey: "getMoreDevtoolsCmd",
+    oncommand(event) {
+      let window = event.target.ownerDocument.defaultView;
+      window.openUILinkIn("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/", "tab");
+    }
+  },
+];
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -47,9 +47,10 @@ EXTRA_COMPONENTS += [
     'devtools-startup.manifest',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
     'definitions.js',
     'main.js',
+    'menus.js',
 )
--- a/devtools/client/netmonitor/test/browser_net_streaming-response.js
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -43,16 +43,17 @@ function test() {
 
     yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
     let editor = yield NetMonitorView.editor("#response-content-textarea");
 
     testEditorContent(editor, REQUESTS[0]); // the hls-m3u8 part
 
     RequestsMenu.selectedIndex = 1;
     yield panelWin.once(panelWin.EVENTS.TAB_UPDATED);
+    yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
 
     testEditorContent(editor, REQUESTS[1]); // the mpeg-dash part
 
     yield teardown(monitor);
     finish();
   });
 
   function testEditorContent(editor, [ fmt, textRe, mode ]) {
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -38,37 +38,58 @@ let bootstrap = {
     // Load a special UA stylesheet to reset certain styles such as dropdown
     // lists.
     loadSheet(window,
               "resource://devtools/client/responsive.html/responsive-ua.css",
               "agent");
     this.telemetry.toolOpened("responsive");
     let store = this.store = Store();
     let app = App({
-      onExit: () => window.postMessage({type: "exit"}, "*"),
+      onExit: () => window.postMessage({ type: "exit" }, "*"),
     });
     let provider = createElement(Provider, { store }, app);
     ReactDOM.render(provider, document.querySelector("#root"));
+    this.initDevices();
+    window.postMessage({ type: "init" }, "*");
   },
 
   destroy() {
     this.store = null;
     this.telemetry.toolClosed("responsive");
     this.telemetry = null;
   },
 
   /**
    * While most actions will be dispatched by React components, some external
    * APIs that coordinate with the larger browser UI may also have actions to
    * to dispatch.  They can do so here.
    */
   dispatch(action) {
+    if (!this.store) {
+      // If actions are dispatched after store is destroyed, ignore them.  This
+      // can happen in tests that close the tool quickly while async tasks like
+      // initDevices() below are still pending.
+      return;
+    }
     this.store.dispatch(action);
   },
 
+  initDevices() {
+    GetDevices().then(devices => {
+      for (let type of devices.TYPES) {
+        this.dispatch(addDeviceType(type));
+        for (let device of devices[type]) {
+          if (device.os != "fxos") {
+            this.dispatch(addDevice(device, type));
+          }
+        }
+      }
+    });
+  },
+
 };
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   bootstrap.init();
 });
 
 window.addEventListener("unload", function onUnload() {
@@ -86,25 +107,13 @@ Object.defineProperty(window, "store", {
 });
 
 /**
  * Called by manager.js to add the initial viewport based on the original page.
  */
 window.addInitialViewport = contentURI => {
   try {
     bootstrap.dispatch(changeLocation(contentURI));
-
-    GetDevices().then(devices => {
-      for (let type of devices.TYPES) {
-        bootstrap.dispatch(addDeviceType(type));
-        for (let device of devices[type]) {
-          if (device.os != "fxos") {
-            bootstrap.dispatch(addDevice(device, type));
-          }
-        }
-      }
-    });
-
     bootstrap.dispatch(addViewport());
   } catch (e) {
     console.error(e);
   }
 };
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -191,16 +191,17 @@ ResponsiveUI.prototype = {
    * bug 1238160 about <iframe mozbrowser> for more details.
    */
   init: Task.async(function*() {
     let tabBrowser = this.tab.linkedBrowser;
     let contentURI = tabBrowser.documentURI.spec;
     tabBrowser.loadURI(TOOL_URL);
     yield tabLoaded(this.tab);
     let toolWindow = this.toolWindow = tabBrowser.contentWindow;
+    yield waitForMessage(toolWindow, "init");
     toolWindow.addInitialViewport(contentURI);
     toolWindow.addEventListener("message", this);
   }),
 
   destroy() {
     let tabBrowser = this.tab.linkedBrowser;
     tabBrowser.goBack();
     this.window = null;
@@ -221,16 +222,31 @@ ResponsiveUI.prototype = {
       case "exit":
         toolWindow.removeEventListener(event.type, this);
         ResponsiveUIManager.closeIfNeeded(window, tab);
         break;
     }
   },
 };
 
+function waitForMessage(win, type) {
+  let deferred = promise.defer();
+
+  let onMessage = event => {
+    if (event.data.type !== type) {
+      return;
+    }
+    win.removeEventListener("message", onMessage);
+    deferred.resolve();
+  };
+  win.addEventListener("message", onMessage);
+
+  return deferred.promise;
+}
+
 function tabLoaded(tab) {
   let deferred = promise.defer();
 
   function handle(event) {
     if (event.originalTarget != tab.linkedBrowser.contentDocument ||
         event.target.location.href == "about:blank") {
       return;
     }
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -1,9 +1,9 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
-  browser_devices.json
+  devices.json
   head.js
 
 [browser_exit_button.js]
 [browser_viewport_basics.js]
deleted file mode 100644
--- a/devtools/client/responsive.html/test/browser/browser_devices.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "TYPES": [ "phones" ],
-  "phones": [
-    {
-      "name": "Firefox OS Flame",
-      "width": 320,
-      "height": 570,
-      "pixelRatio": 1.5,
-      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
-      "touch": true,
-      "firefoxOS": true,
-      "os": "fxos"
-    },
-    {
-      "name": "Alcatel One Touch Fire",
-      "width": 320,
-      "height": 480,
-      "pixelRatio": 1,
-      "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
-      "touch": true,
-      "firefoxOS": true,
-      "os": "fxos"
-    },
-  ],
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/devices.json
@@ -0,0 +1,25 @@
+{
+  "TYPES": [ "phones" ],
+  "phones": [
+    {
+      "name": "Firefox OS Flame",
+      "width": 320,
+      "height": 570,
+      "pixelRatio": 1.5,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "Alcatel One Touch Fire",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    }
+  ]
+}
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -11,19 +11,21 @@ Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
   this);
 
 const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
 
+SimpleTest.requestCompleteLog();
+
 DevToolsUtils.testing = true;
 Services.prefs.setCharPref("devtools.devices.url",
-  TEST_URI_ROOT + "browser_devices.json");
+  TEST_URI_ROOT + "devices.json");
 Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
   Services.prefs.clearUserPref("devtools.devices.url");
   Services.prefs.clearUserPref("devtools.responsive.html.enabled");
 });
 const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -437,24 +437,24 @@ ResponsiveUI.prototype = {
         break;
     }
   },
 
   /**
    * Check the menu items.
    */
    checkMenus: function RUI_checkMenus() {
-     this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "true");
+     this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true");
    },
 
   /**
    * Uncheck the menu items.
    */
    unCheckMenus: function RUI_unCheckMenus() {
-     this.chromeDoc.getElementById("Tools:ResponsiveUI").setAttribute("checked", "false");
+     this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "false");
    },
 
   /**
    * Build the toolbar and the resizers.
    *
    * <vbox class="browserContainer"> From tabbrowser.xml
    *  <toolbar class="devtools-responsiveui-toolbar">
    *    <menulist class="devtools-responsiveui-menulist"/> // presets
--- a/devtools/client/responsivedesign/test/browser_responsiveruleview.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
@@ -76,20 +76,20 @@ function* testEscapeOpensSplitConsole(in
   let onSplit = inspector._toolbox.once("split-console");
   EventUtils.synthesizeKey("VK_ESCAPE", {});
   yield onSplit;
 
   ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
 }
 
 function* testMenuItem(rdm) {
-  is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
+  is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
      "true", "The menu item is checked");
 
   yield closeRDM(rdm);
 
-  is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
+  is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
      "false", "The menu item is unchecked");
 }
 
 function numberOfRules(ruleView) {
   return ruleView.element.querySelectorAll(".ruleview-code").length;
 }
--- a/devtools/client/responsivedesign/test/browser_responsiveui.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveui.js
@@ -5,17 +5,17 @@
 
 add_task(function*() {
   let tab = yield addTab("data:text/html,mop");
 
   let {rdm, manager} = yield openRDM(tab, "menu");
   let container = gBrowser.getBrowserContainer();
   is(container.getAttribute("responsivemode"), "true",
      "Should be in responsive mode.");
-  is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
+  is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
      "true", "Menu item should be checked");
 
   ok(rdm, "An instance of the RDM should be attached to the tab.");
 
   let originalWidth = (yield getSizing()).width;
 
   let documentLoaded = waitForDocLoadComplete();
   gBrowser.loadURI("data:text/html;charset=utf-8,mop" +
@@ -55,26 +55,26 @@ add_task(function*() {
   yield resized;
 
   let currentSize = yield getSizing();
   is(currentSize.width, widthBeforeClose, "width should be restored");
   is(currentSize.height, heightBeforeClose, "height should be restored");
 
   container = gBrowser.getBrowserContainer();
   is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
-  is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
+  is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
      "true", "menu item should be checked");
 
   let isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
   if (!isWinXP) {
     yield testScreenshot(rdm);
   }
 
   yield closeRDM(rdm);
-  is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
+  is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
      "false", "menu item should be unchecked");
 });
 
 function* testPresets(rdm, manager) {
   // Starting from length - 4 because last 3 items are not presets :
   // the separator, the add button and the remove button
   for (let c = rdm.menulist.firstChild.childNodes.length - 4; c >= 0; c--) {
     let item = rdm.menulist.firstChild.childNodes[c];
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -34,17 +34,17 @@ const { ResponsiveUIManager } = Cu.impor
  */
 var openRDM = Task.async(function*(tab = gBrowser.selectedTab,
                                    method = "menu") {
   let manager = ResponsiveUIManager;
 
   let opened = once(manager, "on");
   let resized = once(manager, "contentResize");
   if (method == "menu") {
-    document.getElementById("Tools:ResponsiveUI").doCommand();
+    document.getElementById("menu_responsiveUI").doCommand();
   } else {
     synthesizeKeyFromKeyTag(document.getElementById("key_responsiveUI"));
   }
   yield opened;
 
   let rdm = manager.getResponsiveUIForTab(tab);
   rdm.transitionsEnabled = false;
   registerCleanupFunction(() => {
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -12,9 +12,10 @@ DevToolsModules(
     'object-box.js',
     'object-link.js',
     'object.js',
     'rep-utils.js',
     'rep.js',
     'reps.css',
     'string.js',
     'undefined.js',
+    'url.js',
 )
--- a/devtools/client/shared/components/reps/reps.css
+++ b/devtools/client/shared/components/reps/reps.css
@@ -41,16 +41,21 @@
 }
 
 .objectLink-function,
 .objectBox-stackTrace,
 .objectLink-profile {
   color: DarkGreen;
 }
 
+.objectLink-Location {
+  font-style: italic;
+  color: #555555;
+}
+
 .objectBox-null,
 .objectBox-undefined,
 .objectBox-hint,
 .logRowHint {
   font-style: italic;
   color: #787878;
 }
 
--- a/devtools/client/shared/components/reps/string.js
+++ b/devtools/client/shared/components/reps/string.js
@@ -45,17 +45,17 @@ define(function(require, exports, module
   }
 
   function cropMultipleLines(text, limit) {
     return escapeNewLines(cropString(text, limit));
   }
 
   function cropString(text, limit, alternativeText) {
     if (!alternativeText) {
-      alternativeText = "...";
+      alternativeText = "\u2026";
     }
 
     // Make sure it's a string.
     text = text + "";
 
     // Use default limit if necessary.
     if (!limit) {
       limit = 50;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/reps/url.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global URLSearchParams, URL */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function(require, exports, module) {
+  function parseURLParams(url) {
+    url = new URL(url);
+    return parseURLEncodedText(url.searchParams);
+  }
+
+  function parseURLEncodedText(text) {
+    let params = [];
+
+    // In case the text is empty just return the empty parameters
+    if (text == "") {
+      return params;
+    }
+
+    let searchParams = new URLSearchParams(text);
+    let entries = [...searchParams.entries()];
+    return entries.map(entry => {
+      return {
+        name: entry[0],
+        value: entry[1]
+      };
+    });
+  }
+
+  // Exports from this module
+  exports.parseURLParams = parseURLParams;
+  exports.parseURLEncodedText = parseURLEncodedText;
+});
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -24,16 +24,17 @@ loader.lazyGetter(this, "prefBranch", fu
 });
 loader.lazyGetter(this, "toolboxStrings", function () {
   return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 });
 
 loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index");
 loader.lazyRequireGetter(this, "util", "gcli/util/util");
 loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/shared/webconsole/utils", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 
 /**
  * A collection of utilities to help working with commands
  */
 var CommandUtils = {
   /**
    * Utility to ensure that things are loaded in the correct order
    */
@@ -296,35 +297,42 @@ DeveloperToolbar.prototype.createToolbar
   let toolbar = this._doc.createElement("toolbar");
   toolbar.setAttribute("id", "developer-toolbar");
   toolbar.setAttribute("hidden", "true");
 
   let close = this._doc.createElement("toolbarbutton");
   close.setAttribute("id", "developer-toolbar-closebutton");
   close.setAttribute("class", "close-icon");
   close.setAttribute("oncommand", "DeveloperToolbar.hide();");
-  close.setAttribute("tooltiptext", "developerToolbarCloseButton.tooltiptext");
+  let closeTooltip = toolboxStrings.GetStringFromName("toolbar.closeButton.tooltip");
+  close.setAttribute("tooltiptext", closeTooltip);
 
   let stack = this._doc.createElement("stack");
   stack.setAttribute("flex", "1");
 
   let input = this._doc.createElement("textbox");
   input.setAttribute("class", "gclitoolbar-input-node");
   input.setAttribute("rows", "1");
   stack.appendChild(input);
 
   let hbox = this._doc.createElement("hbox");
   hbox.setAttribute("class", "gclitoolbar-complete-node");
   stack.appendChild(hbox);
 
   let toolboxBtn = this._doc.createElement("toolbarbutton");
   toolboxBtn.setAttribute("id", "developer-toolbar-toolbox-button");
   toolboxBtn.setAttribute("class", "developer-toolbar-button");
-  toolboxBtn.setAttribute("observes", "devtoolsMenuBroadcaster_DevToolbox");
-  toolboxBtn.setAttribute("tooltiptext", "devToolbarToolsButton.tooltip");
+  let toolboxTooltip = toolboxStrings.GetStringFromName("toolbar.toolsButton.tooltip");
+  toolboxBtn.setAttribute("tooltiptext", toolboxTooltip);
+  toolboxBtn.addEventListener("command", function (event) {
+    let window = event.target.ownerDocument.defaultView;
+    gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+  });
+  this._errorCounterButton = toolboxBtn;
+  this._errorCounterButton._defaultTooltipText = toolboxTooltip;
 
   // On Mac, the close button is on the left,
   // while it is on the right on every other platforms.
   if (isMac) {
     toolbar.appendChild(close);
     toolbar.appendChild(stack);
     toolbar.appendChild(toolboxBtn);
   } else {
@@ -337,19 +345,16 @@ DeveloperToolbar.prototype.createToolbar
   let bottomBox = this._doc.getElementById("browser-bottombox");
   if (bottomBox) {
     bottomBox.appendChild(this._element);
   } else { // SeaMonkey does not have a "browser-bottombox".
     let statusBar = this._doc.getElementById("status-bar");
     if (statusBar)
       statusBar.parentNode.insertBefore(this._element, statusBar);
   }
-  this._errorCounterButton = toolboxBtn
-  this._errorCounterButton._defaultTooltipText =
-      this._errorCounterButton.getAttribute("tooltiptext");
 };
 
 /**
  * Called from browser.xul in response to menu-click or keyboard shortcut to
  * toggle the toolbar
  */
 DeveloperToolbar.prototype.toggle = function() {
   if (this.visible) {
@@ -427,17 +432,17 @@ DeveloperToolbar.prototype.show = functi
     // write to, so this needs to be done asynchronously.
     let panelPromises = [
       TooltipPanel.create(this),
       OutputPanel.create(this)
     ];
     return promise.all(panelPromises).then(panels => {
       [ this.tooltipPanel, this.outputPanel ] = panels;
 
-      this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true");
+      this._doc.getElementById("menu_devToolbar").setAttribute("checked", "true");
 
       this.target = TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab);
       const options = {
         environment: CommandUtils.createEnvironment(this, "target"),
         document: this.outputPanel.document,
       };
       return CommandUtils.createRequisition(this.target, options).then(requisition => {
         this.requisition = requisition;
@@ -549,17 +554,17 @@ DeveloperToolbar.prototype.hide = functi
   // show() is async, so ensure we don't need to wait for show() to finish
   var waitPromise = this._showPromise || promise.resolve();
 
   this._hidePromise = waitPromise.then(() => {
     this._element.hidden = true;
 
     Services.prefs.setBoolPref("devtools.toolbar.visible", false);
 
-    this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "false");
+    this._doc.getElementById("menu_devToolbar").setAttribute("checked", "false");
     this.destroy();
 
     this._telemetry.toolClosed("developertoolbar");
     this._notify(NOTIFICATIONS.HIDE);
 
     this._hidePromise = null;
   });
 
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -86,16 +86,20 @@ const { findMostRelevantCssPropertyIndex
  *       focusable element.
  *    {Boolean} stopOnShiftTab:
  *       If true, shift tab will not advance the editor to the previous
  *       focusable element.
  *    {String} trigger: The DOM event that should trigger editing,
  *      defaults to "click"
  *    {Boolean} multiline: Should the editor be a multiline textarea?
  *      defaults to false
+ *    {Function or Number} maxWidth:
+ *       Should the editor wrap to remain below the provided max width. Only
+ *       available if multiline is true. If a function is provided, it will be
+ *       called when replacing the element by the inplace input.
  *    {Boolean} trimOutput: Should the returned string be trimmed?
  *      defaults to true
  *    {Boolean} preserveTextStyles: If true, do not copy text-related styles
  *              from `element` to the new input.
  *      defaults to false
  */
 function editableField(options) {
   return editableItem(options, function(element, event) {
@@ -194,16 +198,21 @@ function InplaceEditor(options, event) {
   this.doc = doc;
   this.elt.inplaceEditor = this;
 
   this.change = options.change;
   this.done = options.done;
   this.destroy = options.destroy;
   this.initial = options.initial ? options.initial : this.elt.textContent;
   this.multiline = options.multiline || false;
+  this.maxWidth = options.maxWidth;
+  if (typeof this.maxWidth == "function") {
+    this.maxWidth = this.maxWidth();
+  }
+
   this.trimOutput = options.trimOutput === undefined
                     ? true
                     : !!options.trimOutput;
   this.stopOnShiftTab = !!options.stopOnShiftTab;
   this.stopOnTab = !!options.stopOnTab;
   this.stopOnReturn = !!options.stopOnReturn;
   this.contentType = options.contentType || CONTENT_TYPES.PLAIN_TEXT;
   this.property = options.property;
@@ -213,37 +222,39 @@ function InplaceEditor(options, event) {
                           : !!options.preserveTextStyles;
 
   this._onBlur = this._onBlur.bind(this);
   this._onKeyPress = this._onKeyPress.bind(this);
   this._onInput = this._onInput.bind(this);
   this._onKeyup = this._onKeyup.bind(this);
 
   this._createInput();
+
+  // Hide the provided element and add our editor.
+  this.originalDisplay = this.elt.style.display;
+  this.elt.style.display = "none";
+  this.elt.parentNode.insertBefore(this.input, this.elt);
+
+  // After inserting the input to have all CSS styles applied, start autosizing.
   this._autosize();
-  this.inputCharWidth = this._getInputCharWidth();
 
+  this.inputCharDimensions = this._getInputCharDimensions();
   // Pull out character codes for advanceChars, listing the
   // characters that should trigger a blur.
   if (typeof options.advanceChars === "function") {
     this._advanceChars = options.advanceChars;
   } else {
     let advanceCharcodes = {};
     let advanceChars = options.advanceChars || "";
     for (let i = 0; i < advanceChars.length; i++) {
       advanceCharcodes[advanceChars.charCodeAt(i)] = true;
     }
     this._advanceChars = charCode => charCode in advanceCharcodes;
   }
 
-  // Hide the provided element and add our editor.
-  this.originalDisplay = this.elt.style.display;
-  this.elt.style.display = "none";
-  this.elt.parentNode.insertBefore(this.input, this.elt);
-
   this.input.focus();
 
   if (typeof options.selectAll == "undefined" || options.selectAll) {
     this.input.select();
   }
 
   if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
     this._maybeSuggestCompletion(false);
@@ -281,16 +292,23 @@ InplaceEditor.prototype = {
     let val = this.trimOutput ? this.input.value.trim() : this.input.value;
     return val;
   },
 
   _createInput: function() {
     this.input =
       this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
     this.input.inplaceEditor = this;
+
+    if (this.multiline) {
+      // Hide the textarea resize handle.
+      this.input.style.resize = "none";
+      this.input.style.overflow = "hidden";
+    }
+
     this.input.classList.add("styleinspector-propertyeditor");
     this.input.value = this.initial;
     if (!this.preserveTextStyles) {
       copyTextStyles(this.elt, this.input);
     }
   },
 
   /**
@@ -347,16 +365,28 @@ InplaceEditor.prototype = {
       this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
     this._measurement.className = "autosizer";
     this.elt.parentNode.appendChild(this._measurement);
     let style = this._measurement.style;
     style.visibility = "hidden";
     style.position = "absolute";
     style.top = "0";
     style.left = "0";
+
+    if (this.multiline) {
+      style.whiteSpace = "pre-wrap";
+      style.wordWrap = "break-word";
+      if (this.maxWidth) {
+        style.maxWidth = this.maxWidth + "px";
+        // Use position fixed to measure dimensions without any influence from
+        // the container of the editor.
+        style.position = "fixed";
+      }
+    }
+
     copyTextStyles(this.input, this._measurement);
     this._updateSize();
   },
 
   /**
    * Clean up the mess created by _autosize().
    */
   _stopAutosize: function() {
@@ -369,46 +399,59 @@ InplaceEditor.prototype = {
 
   /**
    * Size the editor to fit its current contents.
    */
   _updateSize: function() {
     // Replace spaces with non-breaking spaces.  Otherwise setting
     // the span's textContent will collapse spaces and the measurement
     // will be wrong.
-    this._measurement.textContent = this.input.value.replace(/ /g, "\u00a0");
+    let content = this.input.value;
+    let unbreakableSpace = "\u00a0";
 
-    let width = this._measurement.offsetWidth;
-    if (this.multiline) {
-      // Make sure there's some content in the current line.  This is a hack to
-      // account for the fact that after adding a newline the <pre> doesn't grow
-      // unless there's text content on the line.
-      width += 15;
-      this.input.style.height = this._measurement.offsetHeight + "px";
+    // Make sure the content is not empty.
+    if (content === "") {
+      content = unbreakableSpace;
+    }
+
+    // If content ends with a new line, add a blank space to force the autosize
+    // element to adapt its height.
+    if (content.lastIndexOf("\n") === content.length - 1) {
+      content = content + unbreakableSpace;
     }
 
-    if (width === 0) {
-      // If the editor is empty use a width corresponding to 1 character.
-      this.input.style.width = "1ch";
-    } else {
-      // Add 2 pixels to ensure the caret will be visible
-      width = width + 2;
-      this.input.style.width = width + "px";
+    if (!this.multiline) {
+      content = content.replace(/ /g, unbreakableSpace);
     }
+
+    this._measurement.textContent = content;
+
+    // Do not use offsetWidth: it will round floating width values.
+    let width = this._measurement.getBoundingClientRect().width + 2;
+    if (this.multiline) {
+      if (this.maxWidth) {
+        width = Math.min(this.maxWidth, width);
+      }
+      let height = this._measurement.getBoundingClientRect().height;
+      this.input.style.height = height + "px";
+    }
+    this.input.style.width = width + "px";
   },
 
   /**
-   * Get the width of a single character in the input to properly position the
-   * autocompletion popup.
+   * Get the width and height of a single character in the input to properly
+   * position the autocompletion popup.
    */
-  _getInputCharWidth: function() {
-    // Just make the text content to be 'x' to get the width of any character in
-    // a monospace font.
+  _getInputCharDimensions: function() {
+    // Just make the text content to be 'x' to get the width and height of any
+    // character in a monospace font.
     this._measurement.textContent = "x";
-    return this._measurement.offsetWidth;
+    let width = this._measurement.clientWidth;
+    let height = this._measurement.clientHeight;
+    return { width, height };
   },
 
    /**
    * Increment property values in rule view.
    *
    * @param {Number} increment
    *        The amount to increase/decrease the property value.
    * @return {Boolean} true if value has been incremented.
@@ -1278,44 +1321,99 @@ InplaceEditor.prototype = {
                                               startCheckQuery.length);
         this._updateSize();
       }
 
       // Display the list of suggestions if there are more than one.
       if (finalList.length > 1) {
         // Calculate the popup horizontal offset.
         let indent = this.input.selectionStart - query.length;
-        let offset = indent * this.inputCharWidth;
+        let offset = indent * this.inputCharDimensions.width;
+        offset = this._isSingleLine() ? offset : 0;
 
         // Select the most relevantItem if autoInsert is allowed
         let selectedIndex = autoInsert ? mostRelevantIndex : -1;
 
         // Open the suggestions popup.
         this.popup.setItems(finalList);
         this.popup.openPopup(this.input, offset, 0, selectedIndex);
       } else {
         this.popup.hidePopup();
       }
       // This emit is mainly for the purpose of making the test flow simpler.
       this.emit("after-suggest");
       this._doValidation();
     }, 0);
   },
+
+  /**
+   * Check if the current input is displaying more than one line of text.
+   *
+   * @return {Boolean} true if the input has a single line of text
+   */
+  _isSingleLine: function() {
+    let inputRect = this.input.getBoundingClientRect();
+    return inputRect.height < 2 * this.inputCharDimensions.height;
+  },
 };
 
 /**
  * Copy text-related styles from one element to another.
  */
 function copyTextStyles(from, to) {
   let win = from.ownerDocument.defaultView;
   let style = win.getComputedStyle(from);
-  to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
-  to.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
-  to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
-  to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+  let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+  to.style.fontFamily = getCssText("font-family");
+  to.style.fontSize = getCssText("font-size");
+  to.style.fontWeight = getCssText("font-weight");
+  to.style.fontStyle = getCssText("font-style");
+  to.style.lineHeight = getCssText("line-height");
+
+  // If box-sizing is set to border-box, box model styles also need to be
+  // copied.
+  let boxSizing = getCssText("box-sizing");
+  if (boxSizing === "border-box") {
+    to.style.boxSizing = boxSizing;
+    copyBoxModelStyles(from, to);
+  }
+}
+
+/**
+ * Copy box model styles that can impact width and height measurements when box-
+ * sizing is set to "border-box" instead of "content-box".
+ *
+ * @param {DOMNode} from
+ *        the element from which styles are copied
+ * @param {DOMNode} to
+ *        the element on which copied styles are applied
+ */
+function copyBoxModelStyles(from, to) {
+  let win = from.ownerDocument.defaultView;
+  let style = win.getComputedStyle(from);
+  let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+  // Copy all paddings.
+  to.style.paddingTop = getCssText("padding-top");
+  to.style.paddingRight = getCssText("padding-right");
+  to.style.paddingBottom = getCssText("padding-bottom");
+  to.style.paddingLeft = getCssText("padding-left");
+
+  // Copy border styles.
+  to.style.borderTopStyle = getCssText("border-top-style");
+  to.style.borderRightStyle = getCssText("border-right-style");
+  to.style.borderBottomStyle = getCssText("border-bottom-style");
+  to.style.borderLeftStyle = getCssText("border-left-style");
+
+  // Copy border widths.
+  to.style.borderTopWidth = getCssText("border-top-width");
+  to.style.borderRightWidth = getCssText("border-right-width");
+  to.style.borderBottomWidth = getCssText("border-bottom-width");
+  to.style.borderLeftWidth = getCssText("border-left-width");
 }
 
 /**
  * Trigger a focus change similar to pressing tab/shift-tab.
  */
 function moveFocus(win, direction) {
   return focusManager.moveFocus(win, null, direction, 0);
 }
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {angleUtils} = require("devtools/shared/css-angle");
 const {colorUtils} = require("devtools/shared/css-color");
 const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const BEZIER_KEYWORDS = ["linear", "ease-in-out", "ease-in", "ease-out",
                          "ease"];
 
 // Functions that accept a color argument.
 const COLOR_TAKING_FUNCTIONS = ["linear-gradient",
@@ -334,16 +335,17 @@ OutputParser.prototype = {
       // in order to prevent the value input to be focused.
       // Bug 711942 will add a tooltip to edit angle values and we should
       // be able to move this listener to Tooltip.js when it'll be implemented.
       swatch.addEventListener("click", function(event) {
         if (event.shiftKey) {
           event.stopPropagation();
         }
       }, false);
+      EventEmitter.decorate(swatch);
       container.appendChild(swatch);
     }
 
     let value = this._createNode("span", {
       class: options.angleClass
     }, angle);
 
     container.appendChild(value);
@@ -391,16 +393,17 @@ OutputParser.prototype = {
 
       if (options.colorSwatchClass) {
         let swatch = this._createNode("span", {
           class: options.colorSwatchClass,
           style: "background-color:" + color
         });
         this.colorSwatches.set(swatch, colorObj);
         swatch.addEventListener("mousedown", this._onColorSwatchMouseDown, false);
+        EventEmitter.decorate(swatch);
         container.appendChild(swatch);
       }
 
       if (options.defaultColorType) {
         color = colorObj.toString();
         container.dataset.color = color;
       }
 
@@ -457,31 +460,33 @@ OutputParser.prototype = {
       return;
     }
 
     let swatch = event.target;
     let color = this.colorSwatches.get(swatch);
     let val = color.nextColorUnit();
 
     swatch.nextElementSibling.textContent = val;
+    swatch.emit("unit-change", val);
   },
 
   _onAngleSwatchMouseDown: function(event) {
     // Prevent text selection in the case of shift-click or double-click.
     event.preventDefault();
 
     if (!event.shiftKey) {
       return;
     }
 
     let swatch = event.target;
     let angle = this.angleSwatches.get(swatch);
     let val = angle.nextAngleUnit();
 
     swatch.nextElementSibling.textContent = val;
+    swatch.emit("unit-change", val);
   },
 
   /**
    * A helper function that sanitizes a possibly-unterminated URL.
    */
   _sanitizeURL: function(url) {
     // Re-lex the URL and add any needed termination characters.
     let urlTokenizer = DOMUtils.getCSSLexer(url);
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -107,16 +107,17 @@ skip-if = e10s # Bug 1221911, bug 122228
 [browser_graphs-14.js]
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_graphs-15.js]
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_graphs-16.js]
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
+[browser_inplace-editor_maxwidth.js]
 [browser_layoutHelpers.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_mdn-docs-01.js]
 [browser_mdn-docs-02.js]
 [browser_mdn-docs-03.js]
 [browser_num-l10n.js]
--- a/devtools/client/shared/test/browser_css_angle.js
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -54,23 +54,23 @@ function getTestData() {
     authored: "0deg",
     deg: "0deg",
     rad: "0rad",
     grad: "0grad",
     turn: "0turn"
   }, {
     authored: "180deg",
     deg: "180deg",
-    rad: "3.14rad",
+    rad: `${Math.round(Math.PI * 10000) / 10000}rad`,
     grad: "200grad",
     turn: "0.5turn"
   }, {
     authored: "180DEG",
     deg: "180DEG",
-    rad: "3.14RAD",
+    rad: `${Math.round(Math.PI * 10000) / 10000}RAD`,
     grad: "200GRAD",
     turn: "0.5TURN"
   }, {
     authored: `-${Math.PI}rad`,
     deg: "-180deg",
     rad: `-${Math.PI}rad`,
     grad: "-200grad",
     turn: "-0.5turn"
@@ -78,35 +78,35 @@ function getTestData() {
     authored: `-${Math.PI}RAD`,
     deg: "-180DEG",
     rad: `-${Math.PI}RAD`,
     grad: "-200GRAD",
     turn: "-0.5TURN"
   }, {
     authored: "100grad",
     deg: "90deg",
-    rad: "1.57rad",
+    rad: `${Math.round(Math.PI / 2 * 10000) / 10000}rad`,
     grad: "100grad",
     turn: "0.25turn"
   }, {
     authored: "100GRAD",
     deg: "90DEG",
-    rad: "1.57RAD",
+    rad: `${Math.round(Math.PI / 2 * 10000) / 10000}RAD`,
     grad: "100GRAD",
     turn: "0.25TURN"
   }, {
     authored: "-1turn",
     deg: "-360deg",
-    rad: "-6.28rad",
+    rad: `${-1 * Math.round(Math.PI * 2 * 10000) / 10000}rad`,
     grad: "-400grad",
     turn: "-1turn"
   }, {
     authored: "-10TURN",
     deg: "-3600DEG",
-    rad: "-62.83RAD",
+    rad: `${-1 * Math.round(Math.PI * 2 * 10 * 10000) / 10000}RAD`,
     grad: "-4000GRAD",
     turn: "-10TURN"
   }, {
     authored: "inherit",
     deg: "inherit",
     rad: "inherit",
     grad: "inherit",
     turn: "inherit"
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
@@ -0,0 +1,134 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { editableField } = require("devtools/client/shared/inplace-editor");
+
+const LINE_HEIGHT = 15;
+const MAX_WIDTH = 300;
+const START_TEXT = "Start text";
+const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
+  "I expect the inplace editor to wrap.";
+
+// Test the inplace-editor behavior with a maxWidth configuration option
+// defined.
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8,inplace editor max width tests");
+  let [host, , doc] = yield createHost();
+
+  info("Testing the maxWidth option in pixels, to precisely check the size");
+  yield new Promise(resolve => {
+    createInplaceEditorAndClick({
+      multiline: true,
+      maxWidth: MAX_WIDTH,
+      start: testMaxWidth,
+      done: resolve
+    }, doc);
+  });
+
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
+
+let testMaxWidth = Task.async(function* (editor) {
+  is(editor.input.value, START_TEXT, "Span text content should be used");
+  ok(editor.input.offsetWidth < MAX_WIDTH,
+    "Input width should be strictly smaller than MAX_WIDTH");
+  is(getLines(editor.input), 1, "Input should display 1 line of text");
+
+  info("Check a text is on several lines if it does not fit MAX_WIDTH");
+  for (let key of LONG_TEXT) {
+    EventUtils.sendChar(key);
+    checkScrollbars(editor.input);
+  }
+
+  is(editor.input.value, LONG_TEXT, "Long text should be the input value");
+  is(editor.input.offsetWidth, MAX_WIDTH,
+    "Input width should be the same as MAX_WIDTH");
+  is(getLines(editor.input), 3, "Input should display 3 lines of text");
+  checkScrollbars(editor.input);
+
+  info("Delete all characters on line 3.");
+  while (getLines(editor.input) === 3) {
+    EventUtils.sendKey("BACK_SPACE");
+    checkScrollbars(editor.input);
+  }
+
+  is(editor.input.offsetWidth, MAX_WIDTH,
+    "Input width should be the same as MAX_WIDTH");
+  is(getLines(editor.input), 2, "Input should display 2 lines of text");
+  checkScrollbars(editor.input);
+
+  info("Delete all characters on line 2.");
+  while (getLines(editor.input) === 2) {
+    EventUtils.sendKey("BACK_SPACE");
+    checkScrollbars(editor.input);
+  }
+
+  is(getLines(editor.input), 1, "Input should display 1 line of text");
+  checkScrollbars(editor.input);
+
+  info("Delete all characters.");
+  while (editor.input.value !== "") {
+    EventUtils.sendKey("BACK_SPACE");
+    checkScrollbars(editor.input);
+  }
+
+  ok(editor.input.offsetWidth < MAX_WIDTH,
+    "Input width should again be strictly smaller than MAX_WIDTH");
+  ok(editor.input.offsetWidth > 0,
+    "Even with no content, the input has a non-zero width");
+  is(getLines(editor.input), 1, "Input should display 1 line of text");
+  checkScrollbars(editor.input);
+
+  info("Leave the inplace-editor");
+  EventUtils.sendKey("RETURN");
+});
+
+/**
+ * Retrieve the current number of lines displayed in the provided textarea.
+ *
+ * @param {DOMNode} textarea
+ * @return {Number} the number of lines
+ */
+function getLines(textarea) {
+  return Math.floor(textarea.clientHeight / LINE_HEIGHT);
+}
+
+/**
+ * Verify that the provided textarea has no vertical or horizontal scrollbar.
+ *
+ * @param {DOMNode} textarea
+ */
+function checkScrollbars(textarea) {
+  is(textarea.scrollHeight, textarea.clientHeight,
+    "Textarea should never have vertical scrollbars");
+  is(textarea.scrollWidth, textarea.clientWidth,
+    "Textarea should never have horizontal scrollbars");
+}
+
+function createInplaceEditorAndClick(options, doc) {
+  doc.body.innerHTML = "";
+  let span = options.element = createSpan(doc);
+
+  info("Creating an inplace-editor field");
+  editableField(options);
+
+  info("Clicking on the inplace-editor field to turn to edit mode");
+  span.click();
+}
+
+function createSpan(doc) {
+  info("Creating a new span element");
+  let span = doc.createElement("span");
+  span.setAttribute("tabindex", "0");
+  span.style.lineHeight = LINE_HEIGHT + "px";
+  span.style.fontSize = "11px";
+  span.style.fontFamily = "monospace";
+  span.textContent = START_TEXT;
+  doc.body.appendChild(span);
+  return span;
+}
--- a/devtools/client/shared/test/browser_toolbar_basic.js
+++ b/devtools/client/shared/test/browser_toolbar_basic.js
@@ -7,45 +7,45 @@ const TEST_URI = TEST_URI_ROOT + "browse
 
 add_task(function*() {
   info("Starting browser_toolbar_basic.js");
   yield addTab(TEST_URI);
 
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in to start");
 
   let shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
-  document.getElementById("Tools:DevToolbar").doCommand();
+  document.getElementById("menu_devToolbar").doCommand();
   yield shown;
   ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
 
   let close = document.getElementById("developer-toolbar-closebutton");
   ok(close, "Close button exists");
 
   let toggleToolbox =
-    document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+    document.getElementById("menu_devToolbox");
   ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = yield gDevTools.showToolbox(target, "inspector");
   ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
 
   yield addTab("about:blank");
   info("Opened a new tab");
 
   ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
 
   gBrowser.removeCurrentTab();
 
   let hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
-  document.getElementById("Tools:DevToolbar").doCommand();
+  document.getElementById("menu_devToolbar").doCommand();
   yield hidden;
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hidden");
 
   shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
-  document.getElementById("Tools:DevToolbar").doCommand();
+  document.getElementById("menu_devToolbar").doCommand();
   yield shown;
   ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in after open");
 
   ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
 
   hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
   document.getElementById("developer-toolbar-closebutton").doCommand();
   yield hidden;
--- a/devtools/client/shared/test/browser_toolbar_tooltip.js
+++ b/devtools/client/shared/test/browser_toolbar_tooltip.js
@@ -21,17 +21,17 @@ registerCleanupFunction(() => {
 add_task(function* showToolbar() {
   yield addTab(TEST_URI);
 
   info("Starting browser_toolbar_tooltip.js");
 
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
 
   let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
-  document.getElementById("Tools:DevToolbar").doCommand();
+  document.getElementById("menu_devToolbar").doCommand();
   yield showPromise;
 });
 
 add_task(function* testDimensions() {
   let tooltipPanel = DeveloperToolbar.tooltipPanel;
 
   DeveloperToolbar.focusManager.helpRequest();
   yield DeveloperToolbar.inputter.setInput('help help');
@@ -80,17 +80,17 @@ add_task(function* testThemes() {
 add_task(function* hideToolbar() {
   info("Ending browser_toolbar_tooltip.js");
   yield DeveloperToolbar.inputter.setInput('');
 
   ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in hideToolbar");
 
   info("Hide toolbar");
   let hidePromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.HIDE);
-  document.getElementById("Tools:DevToolbar").doCommand();
+  document.getElementById("menu_devToolbar").doCommand();
   yield hidePromise;
 
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hideToolbar");
 
   info("Done test");
 });
 
 function getLeftMargin() {
--- a/devtools/client/shared/test/test-actor.js
+++ b/devtools/client/shared/test/test-actor.js
@@ -615,16 +615,27 @@ var TestActor = exports.TestActor = prot
       y: Arg(1, "number"),
       relative: Arg(2, "nullable:boolean"),
     },
     response: {
       value: RetVal("json")
     }
   }),
 
+  /**
+   * Forces the reflow and waits for the next repaint.
+   */
+  reflow: protocol.method(function () {
+    let deferred = promise.defer();
+    this.content.document.documentElement.offsetWidth;
+    this.content.requestAnimationFrame(deferred.resolve);
+
+    return deferred.promise;
+  }),
+
   getNodeRect: protocol.method(Task.async(function* (selector) {
     let node = this._querySelector(selector);
     return getRect(this.content, node, this.content);
   }), {
     request: {
       selector: Arg(0, "string")
     },
     response: {
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -218,16 +218,17 @@ TableWidget.prototype = {
     let itemIndex = column.cellNodes.indexOf(changedField);
     let items = {};
 
     for (let [name, col] of this.columns) {
       items[name] = col.cellNodes[itemIndex].value;
     }
 
     let change = {
+      host: this.host,
       key: uniqueId,
       field: colName,
       oldValue: data.change.oldValue,
       newValue: data.change.newValue,
       items: items
     };
 
     // A rows position in the table can change as the result of an edit. In
--- a/devtools/client/shared/widgets/VariablesView.jsm
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -2533,19 +2533,17 @@ Variable.prototype = Heritage.extend(Sco
     this._displayVariable();
     this._customizeVariable();
     this._prepareTooltips();
     this._setAttributes();
     this._addEventListeners();
 
     if (this._initialDescriptor.enumerable ||
         this._nameString == "this" ||
-        (this._internalItem &&
-         (this._nameString == "<return>" ||
-          this._nameString == "<exception>"))) {
+        this._internalItem) {
       this.ownerView._enum.appendChild(this._target);
       this.ownerView._enumItems.push(this);
     } else {
       this.ownerView._nonenum.appendChild(this._target);
       this.ownerView._nonEnumItems.push(this);
     }
   },
 
@@ -3483,16 +3481,29 @@ VariablesView.stringifiers.byType = {
     }
     return null;
   },
 
   symbol: function(aGrip, aOptions) {
     const name = aGrip.name || "";
     return "Symbol(" + name + ")";
   },
+
+  mapEntry: function(aGrip, {concise}) {
+    let { preview: { key, value }} = aGrip;
+
+    let keyString = VariablesView.getString(key, {
+      concise: true,
+      noStringQuotes: true,
+    });
+    let valueString = VariablesView.getString(value, { concise: true });
+
+    return keyString + " \u2192 " + valueString;
+  },
+
 }; // VariablesView.stringifiers.byType
 
 VariablesView.stringifiers.byObjectClass = {
   Function: function(aGrip, {concise}) {
     // TODO: Bug 948484 - support arrow functions and ES6 generators
 
     let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
     name = VariablesView.getString(name, { noStringQuotes: true });
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -166,45 +166,43 @@ VariablesViewController.prototype = {
   /**
    * Adds pseudo items in case there is too many properties to display.
    * Each item can expand into property slices.
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The property iterator grip.
-   * @param object aIterator
-   *        The property iterator client.
    */
-  _populatePropertySlices: function(aTarget, aGrip, aIterator) {
+  _populatePropertySlices: function(aTarget, aGrip) {
     if (aGrip.count < MAX_PROPERTY_ITEMS) {
       return this._populateFromPropertyIterator(aTarget, aGrip);
     }
 
     // Divide the keys into quarters.
     let items = Math.ceil(aGrip.count / 4);
-
+    let iterator = aGrip.propertyIterator;
     let promises = [];
     for(let i = 0; i < 4; i++) {
       let start = aGrip.start + i * items;
       let count = i != 3 ? items : aGrip.count - i * items;
 
       // Create a new kind of grip, with additional fields to define the slice
       let sliceGrip = {
         type: "property-iterator",
-        propertyIterator: aIterator,
+        propertyIterator: iterator,
         start: start,
         count: count
       };
 
       // Query the name of the first and last items for this slice
       let deferred = promise.defer();
-      aIterator.names([start, start + count - 1], ({ names }) => {
+      iterator.names([start, start + count - 1], ({ names }) => {
         let label = "[" + names[0] + L10N.ellipsis + names[1] + "]";
-        let item = aTarget.addItem(label);
+        let item = aTarget.addItem(label, {}, { internalItem: true });
         item.showArrow();
         this.addExpander(item, sliceGrip);
         deferred.resolve();
       });
       promises.push(deferred.promise);
     }
 
     return promise.all(promises);
@@ -217,17 +215,17 @@ VariablesViewController.prototype = {
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The property iterator grip.
    */
   _populateFromPropertyIterator: function(aTarget, aGrip) {
     if (aGrip.count >= MAX_PROPERTY_ITEMS) {
       // We already started to split, but there is still too many properties, split again.
-      return this._populatePropertySlices(aTarget, aGrip, aGrip.propertyIterator);
+      return this._populatePropertySlices(aTarget, aGrip);
     }
     // We started slicing properties, and the slice is now small enough to be displayed
     let deferred = promise.defer();
     aGrip.propertyIterator.slice(aGrip.start, aGrip.count,
       ({ ownProperties }) => {
         // Add all the variable properties.
         if (Object.keys(ownProperties).length > 0) {
           aTarget.addItems(ownProperties, {
@@ -267,45 +265,45 @@ VariablesViewController.prototype = {
       };
       objectClient.enumProperties(options, ({ iterator }) => {
         let sliceGrip = {
           type: "property-iterator",
           propertyIterator: iterator,
           start: 0,
           count: iterator.count
         };
-        this._populatePropertySlices(aTarget, sliceGrip, iterator)
+        this._populatePropertySlices(aTarget, sliceGrip)
             .then(() => {
           // Then enumerate the rest of the properties, like length, buffer, etc.
           let options = {
             ignoreIndexedProperties: true,
             sort: true,
             query: aQuery
           };
           objectClient.enumProperties(options, ({ iterator }) => {
             let sliceGrip = {
               type: "property-iterator",
               propertyIterator: iterator,
               start: 0,
               count: iterator.count
             };
-            deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
+            deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
           });
         });
       });
     } else {
       // For objects, we just enumerate all the properties sorted by name.
       objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => {
         let sliceGrip = {
           type: "property-iterator",
           propertyIterator: iterator,
           start: 0,
           count: iterator.count
         };
-        deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
+        deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
       });
 
     }
     return deferred.promise;
   },
 
   /**
    * Adds the given prototype in the view.
@@ -328,39 +326,51 @@ VariablesViewController.prototype = {
    * when a scope is expanded or certain variables are hovered.
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function(aTarget, aGrip) {
+    if (aGrip.class === "Promise" && aGrip.promiseState) {
+      const { state, value, reason } = aGrip.promiseState;
+      aTarget.addItem("<state>", { value: state }, { internalItem: true });
+      if (state === "fulfilled") {
+        this.addExpander(
+          aTarget.addItem("<value>", { value }, { internalItem: true }),
+          value);
+      } else if (state === "rejected") {
+        this.addExpander(
+          aTarget.addItem("<reason>", { value: reason }, { internalItem: true }),
+          reason);
+      }
+    } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) {
+      let entriesList = aTarget.addItem("<entries>", {}, { internalItem: true });
+      entriesList.showArrow();
+      this.addExpander(entriesList, {
+        type: "entries-list",
+        obj: aGrip
+      });
+    }
+
     // Fetch properties by slices if there is too many in order to prevent UI freeze.
     if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) {
       return this._populateFromObjectWithIterator(aTarget, aGrip)
                  .then(() => {
                    let deferred = promise.defer();
                    let objectClient = this._getObjectClient(aGrip);
                    objectClient.getPrototype(({ prototype }) => {
                      this._populateObjectPrototype(aTarget, prototype);
                      deferred.resolve();
                    });
                    return deferred.promise;
                  });
     }
 
-    if (aGrip.class === "Promise" && aGrip.promiseState) {
-      const { state, value, reason } = aGrip.promiseState;
-      aTarget.addItem("<state>", { value: state });
-      if (state === "fulfilled") {
-        this.addExpander(aTarget.addItem("<value>", { value }), value);
-      } else if (state === "rejected") {
-        this.addExpander(aTarget.addItem("<reason>", { value: reason }), reason);
-      }
-    }
     return this._populateProperties(aTarget, aGrip);
   },
 
   _populateProperties: function(aTarget, aGrip, aOptions) {
     let deferred = promise.defer();
 
     let objectClient = this._getObjectClient(aGrip);
     objectClient.getPrototypeAndProperties(aResponse => {
@@ -483,16 +493,40 @@ VariablesViewController.prototype = {
     aTarget.addItems(aBindings.variables, {
       // Not all variables need to force sorted properties.
       sorted: VARIABLES_SORTING_ENABLED,
       // Expansion handlers must be set after the properties are added.
       callback: this.addExpander
     });
   },
 
+  _populateFromEntries: function(target, grip) {
+    let objGrip = grip.obj;
+    let objectClient = this._getObjectClient(objGrip);
+
+    return new promise((resolve, reject) => {
+      objectClient.enumEntries((response) => {
+        if (response.error) {
+          // Older server might not support the enumEntries method
+          console.warn(response.error + ": " + response.message);
+          resolve();
+        } else {
+          let sliceGrip = {
+            type: "property-iterator",
+            propertyIterator: response.iterator,
+            start: 0,
+            count: response.iterator.count
+          };
+
+          resolve(this._populatePropertySlices(target, sliceGrip));
+        }
+      });
+    });
+  },
+
   /**
    * Adds an 'onexpand' callback for a variable, lazily handling
    * the addition of new properties.
    *
    * @param Variable aTarget
    *        The variable where the properties will be placed into.
    * @param any aSource
    *        The source to use to populate the target.
@@ -565,16 +599,31 @@ VariablesViewController.prototype = {
 
     let deferred = promise.defer();
     aTarget._fetched = deferred.promise;
 
     if (aSource.type === "property-iterator") {
       return this._populateFromPropertyIterator(aTarget, aSource);
     }
 
+    if (aSource.type === "entries-list") {
+      return this._populateFromEntries(aTarget, aSource);
+    }
+
+    if (aSource.type === "mapEntry") {
+      aTarget.addItems({
+        key: { value: aSource.preview.key },
+        value: { value: aSource.preview.value }
+      }, {
+        callback: this.addExpander
+      });
+
+      return promise.resolve();
+    }
+
     // If the target is a Variable or Property then we're fetching properties.
     if (VariablesView.isVariable(aTarget)) {
       this._populateFromObject(aTarget, aSource).then(() => {
         // Signal that properties have been fetched.
         this.view.emit("fetched", "properties", aTarget);
         // Commit the hierarchy because new items were added.
         this.view.commitHierarchy();
         deferred.resolve();
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -1,23 +1,28 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
+  storage-complex-values.html
   storage-cookies.html
-  storage-complex-values.html
   storage-listings.html
+  storage-localstorage.html
   storage-overflow.html
   storage-search.html
   storage-secured-iframe.html
+  storage-sessionstorage.html
   storage-unsecured-iframe.html
   storage-updates.html
   head.js
 
 [browser_storage_basic.js]
-[browser_storage_dynamic_updates.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
+[browser_storage_dynamic_updates.js]
+[browser_storage_localstorage_edit.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
+skip-if = os == "linux" && e10s # Bug 1240804 - unhandled promise rejections
+[browser_storage_sessionstorage_edit.js]
 [browser_storage_sidebar.js]
 [browser_storage_values.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_edit.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-localstorage.html");
+
+  yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+  yield gUI.table.once(TableWidget.EVENTS.FIELDS_EDITABLE);
+
+  yield editCell("TestLS1", "name", "newTestLS1");
+  yield editCell("newTestLS1", "value", "newValueLS1");
+
+  yield editCell("TestLS3", "name", "newTestLS3");
+  yield editCell("newTestLS3", "value", "newValueLS3");
+
+  yield editCell("TestLS5", "name", "newTestLS5");
+  yield editCell("newTestLS5", "value", "newValueLS5");
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_edit.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-sessionstorage.html");
+
+  yield selectTreeItem(["sessionStorage", "http://test1.example.org"]);
+  yield gUI.table.once(TableWidget.EVENTS.FIELDS_EDITABLE);
+
+  yield editCell("TestSS1", "name", "newTestSS1");
+  yield editCell("newTestSS1", "value", "newValueSS1");
+
+  yield editCell("TestSS3", "name", "newTestSS3");
+  yield editCell("newTestSS3", "value", "newValueSS3");
+
+  yield editCell("TestSS5", "name", "newTestSS5");
+  yield editCell("newTestSS5", "value", "newValueSS5");
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-localstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+  <!--
+  Bug 1231155 - Storage inspector front end - tests
+  -->
+  <head>
+    <meta charset="utf-8" />
+    <title>Storage inspector localStorage test</title>
+    <script type="application/javascript;version=1.7">
+      "use strict";
+
+      function setup() {
+        localStorage.setItem("TestLS1", "ValueLS1");
+        localStorage.setItem("TestLS2", "ValueLS2");
+        localStorage.setItem("TestLS3", "ValueLS3");
+        localStorage.setItem("TestLS4", "ValueLS4");
+        localStorage.setItem("TestLS5", "ValueLS5");
+      }
+    </script>
+  </head>
+  <body onload="setup()">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-sessionstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+  <!--
+  Bug 1231179 - Storage inspector front end - tests
+  -->
+  <head>
+    <meta charset="utf-8" />
+    <title>Storage inspector sessionStorage test</title>
+    <script type="application/javascript;version=1.7">
+      "use strict";
+
+      function setup() {
+        sessionStorage.setItem("TestSS1", "ValueSS1");
+        sessionStorage.setItem("TestSS2", "ValueSS2");
+        sessionStorage.setItem("TestSS3", "ValueSS3");
+        sessionStorage.setItem("TestSS4", "ValueSS4");
+        sessionStorage.setItem("TestSS5", "ValueSS5");
+      }
+    </script>
+  </head>
+  <body onload="setup()">
+  </body>
+</html>
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -148,16 +148,18 @@ StorageUI.prototype = {
   },
 
   makeFieldsEditable: function() {
     let actor = this.getCurrentActor();
 
     if (typeof actor.getEditableFields !== "undefined") {
       actor.getEditableFields().then(fields => {
         this.table.makeFieldsEditable(fields);
+      }).catch(() => {
+        // Do nothing
       });
     } else if (this.table._editableFieldsEngine) {
       this.table._editableFieldsEngine.destroy();
     }
   },
 
   editItem: function(eventType, data) {
     let actor = this.getCurrentActor();
@@ -370,16 +372,17 @@ StorageUI.prototype = {
     storageType.getStoreObjects(host, names, fetchOpts).then(({data}) => {
       if (!data.length) {
         this.emit("store-objects-updated");
         return;
       }
       if (this.shouldResetColumns) {
         this.resetColumns(data[0], type);
       }
+      this.table.host = host;
       this.populateTable(data, reason);
       this.emit("store-objects-updated");
 
       this.makeFieldsEditable();
     }, Cu.reportError);
   },
 
   /**
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/geometry-editor.svg
@@ -0,0 +1,4 @@
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="#babec3">
+  <path d="M14,8 L12,8 L12,11.25 L12,12 L11.5,12 L3.5,12 L3,12 L3,11.75 L3,11.5 L3,8 L1,8 L1,8 L1,8.5 L1,9 L0,9 L0,8.5 L0,6.5 L0,6 L1,6 L1,6.5 L1,7 L3,7 L3,3.5 L3,3 L3.72222222,3 L3.72222222,3 L10.5555556,3 L11,3 L11,4 L10.5555556,4 L4,4 L4,11 L11,11 L11,3.5 L11,3 L12,3 L12,3.5 L12,7 L14,7 L14,6.5 L14,6 L15,6 L15,6.5 L15,8.5 L15,9 L14,9 L14,8.5 L14,8 Z M8,14 L8.5,14 L9,14 L9,15 L8.5,15 L6.5,15 L6,15 L6,14 L6.5,14 L7,14 L7,11.5 L7,11 L8,11 L8,11.5 L8,14 Z M7,1 L6.5,1 L6,1 L6,0 L6.5,0 L8.5,0 L9,0 L9,1 L8.5,1 L8,1 L8,3.5 L8,4 L7,4 L7,3.5 L7,1 L7,1 Z"/>
+  <path d="M3.5,9 C4.32842712,9 5,8.32842712 5,7.5 C5,6.67157288 4.32842712,6 3.5,6 C2.67157288,6 2,6.67157288 2,7.5 C2,8.32842712 2.67157288,9 3.5,9 Z M7.5,13 C8.32842712,13 9,12.3284271 9,11.5 C9,10.6715729 8.32842712,10 7.5,10 C6.67157288,10 6,10.6715729 6,11.5 C6,12.3284271 6.67157288,13 7.5,13 Z M11.5,9 C12.3284271,9 13,8.32842712 13,7.5 C13,6.67157288 12.3284271,6 11.5,6 C10.6715729,6 10,6.67157288 10,7.5 C10,8.32842712 10.6715729,9 11.5,9 Z M7.5,5 C8.32842712,5 9,4.32842712 9,3.5 C9,2.67157288 8.32842712,2 7.5,2 C6.67157288,2 6,2.67157288 6,3.5 C6,4.32842712 6.67157288,5 7.5,5 Z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -331,8 +331,21 @@
 
 /* Hide all values when the view is inactive */
 
 #layout-container.inactive > #layout-header > #layout-element-position,
 #layout-container.inactive > #layout-header > #layout-element-size,
 #layout-container.inactive > #layout-main > p {
    visibility: hidden;
 }
+
+#layout-position-group {
+  display: flex;
+  align-items: center;
+}
+
+#layout-geometry-editor {
+  visibility: hidden;
+}
+
+#layout-geometry-editor::before {
+  background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
+}
--- a/devtools/client/themes/markup.css
+++ b/devtools/client/themes/markup.css
@@ -27,18 +27,18 @@ body {
  * This allows long overflows of text or input fields to still be styled with
  * the container, rather than the background disappearing when scrolling */
 #root {
   float: left;
   min-width: 100%;
 }
 
 /* Don't display a parent-child outline for the root elements */
-#root > ul > li > .children::before {
-  display: none;
+#root > ul > li > .children {
+  background: none;
 }
 
 body.dragging .tag-line {
   cursor: grabbing;
 }
 
 #root-wrapper:after {
    content: "";
@@ -74,17 +74,16 @@ body.dragging .tag-line {
   margin: 0;
   padding: 0;
 }
 
 .children {
   list-style: none;
   padding: 0;
   margin: 0;
-  position: relative;
 }
 
 /* Tags are organized in a UL/LI tree and indented thanks to a left padding.
  * A very large padding is used in combination with a slightly smaller margin
  * to make sure childs actually span from edge-to-edge. */
 .child {
   margin-left: -1000em;
   padding-left: 1001em;
@@ -131,30 +130,27 @@ ul.children + .tag-line::before {
 .tag-line {
   min-height: 1.4em;
   line-height: 1.4em;
   position: relative;
   cursor: default;
   padding-left: 2px;
 }
 
-.tag-line[selected] + .children::before {
-  content: "";
-  background: var(--markup-outline);
-  width: 1.5px;
-  height: calc(100% - 4px);
-  left: -6px;
-  top: 2px;
-  border-radius: 2px;
-  position: absolute;
-  display: block;
-}
-
-.tag-line:hover:not([selected]) + .children::before {
-  opacity: 0.5;
+.tag-line[selected] + .children {
+  background-image: linear-gradient(to top, var(--markup-outline), var(--markup-outline));
+  background-repeat: no-repeat;
+  /* Shorten the outline height by 4px to account for the 2px top padding and
+   * allow for a 2px bottom padding */
+  background-size: 1.5px calc(100% - 4px);
+  /* Align the outline to under the expander arrow and provide 2px top
+   * padding */
+  background-position: -6px 2px;
+  border-left: 6px solid transparent;
+  margin-left: -6px;
 }
 
 .html-editor-container {
   position: relative;
   min-height: 200px;
 }
 
 /* This extra element placed in each tag is positioned absolutely to cover the
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -204,25 +204,26 @@ a {
 .theme-selected .console-string,
 .theme-selected .cm-number,
 .theme-selected .cm-variable,
 .theme-selected .kind-ArrayLike {
   color: #f5f7fa !important; /* Selection Text Color */
 }
 
 .message[category=network] > .indent {
-  -moz-border-end: solid #000 6px;
+  -moz-border-end: solid var(--theme-body-color-alt) 6px;
 }
 
 .message[category=network][severity=error] > .icon::before {
   background-position: -12px 0;
 }
 
 .message[category=network] > .message-body {
   display: flex;
+  flex-wrap: wrap;
 }
 
 .message[category=network] .method {
   flex: none;
 }
 
 .message[category=network]:not(.navigation-marker) .url {
   flex: 1 1 auto;
@@ -251,17 +252,18 @@ a {
 .message[category=network] .xhr {
   background-color: var(--theme-body-color-alt);
   color: var(--theme-body-background);
   border-radius: 3px;
   font-weight: bold;
   font-size: 10px;
   padding: 2px;
   line-height: 10px;
-  -moz-margin-end: 1ex;
+  margin-inline-start: 3px;
+  margin-inline-end: 1ex;
 }
 
 /* CSS styles */
 .webconsole-filter-button[category="css"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#2DC3F3, #00B6F0);
   border-color: #1BA2CC;
 }
 
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -2774,17 +2774,17 @@ Widgets.ObjectRenderers.add({
       shown++;
     }
 
     this._text(")");
   },
 
   _onClick: function () {
     let location = this.objectActor.location;
-    if (location) {
+    if (location && IGNORED_SOURCE_URLS.indexOf(location.url) === -1) {
       this.output.openLocationInDebugger(location);
     }
     else {
       this.openObjectInVariablesView();
     }
   }
 }); // Widgets.ObjectRenderers.byClass.Function
 
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -584,21 +584,23 @@ WebConsole.prototype = {
       let popupset = this.mainPopupSet;
       let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
       for (let panel of panels) {
         panel.hidePopup();
       }
     }
 
     let onDestroy = Task.async(function*() {
-      try {
-        yield this.target.activeTab.focus()
-      }
-      catch (ex) {
-        // Tab focus can fail if the tab or target is closed.
+      if (!this._browserConsole) {
+        try {
+          yield this.target.activeTab.focus();
+        }
+        catch (ex) {
+          // Tab focus can fail if the tab or target is closed.
+        }
       }
 
       let id = WebConsoleUtils.supportsString(this.hudId);
       Services.obs.notifyObservers(id, "web-console-destroyed", null);
       this._destroyer.resolve(null);
     }.bind(this));
 
     if (this.ui) {
--- a/devtools/client/webconsole/moz.build
+++ b/devtools/client/webconsole/moz.build
@@ -1,16 +1,20 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
+DIRS += [
+    'net'
+]
+
 DevToolsModules(
     'console-commands.js',
     'console-output.js',
     'hudservice.js',
     'jsterm.js',
     'panel.js',
     'webconsole.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/net/.eslintrc
@@ -0,0 +1,18 @@
+{
+  "globals": {
+    "Locale": true,
+    "Document": true,
+    "document": true,
+    "Node": true,
+    "Element": true,
+    "MessageEvent": true,
+    "BrowserLoader": true,
+    "addEventListener": true,
+    "DOMParser": true,
+    "dispatchEvent": true,
+    "setTimeout": true
+  },
+  "rules": {
+    "no-unused-vars": [2, {"args": "none"}],
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/net/components/cookies-tab.js
@@ -0,0 +1,69 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Cookies' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * sent and received cookies.
+ */
+var CookiesTab = React.createClass({
+  propTypes: {
+    actions: PropTypes.shape({
+      requestData: PropTypes.func.isRequired
+    }),
+    data: PropTypes.object.isRequired,
+  },
+
+  displayName: "CookiesTab",
+
+  render() {
+    let actions = this.props.actions;
+    let file = this.props.data;
+
+    let cookies = file.request.cookies;
+    if (!cookies || !cookies.length) {
+      // TODO: use async action objects as soon as Redux is in place
+      actions.requestData("requestCookies");
+
+      return (
+        Spinner()
+      );
+    }
+
+    // The cookie panel displays two groups of cookies:
+    // 1) Response Cookies
+    // 2) Request Cookies
+    let groups = [{
+      key: "responseCookies",
+      name: Locale.$STR("responseCookies"),
+      params: file.response.cookies
+    }, {
+      key: "requestCookies",
+      name: Locale.$STR("requestCookies"),
+      params: file.request.cookies
+    }];
+
+    return (
+      DOM.div({className: "cookiesTabBox"},
+        DOM.div({className: "panelContent"},
+          NetInfoGroupList({
+            groups: groups
+          })
+        )
+      )
+    );
+  }
+});
+
+// Exports from this module
+module.exports = CookiesTab;
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/net/components/headers-tab.js
@@ -0,0 +1,73 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Headers' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * request and response HTTP headers.
+ */
+var HeadersTab = React.createClass({
+  propTypes: {
+    actions: PropTypes.shape({
+      requestData: PropTypes.func.isRequired
+    }),
+    data: PropTypes.object.isRequired,
+  },
+
+  displayName: "HeadersTab",
+
+  render() {
+    let {data, actions} = this.props;
+    let responseHeaders = data.response.headers;
+    let requestHeaders = data.request.headers;
+
+    // Request headers if they are not available yet.
+    // TODO: use async action objects as soon as Redux is in place
+    if (!requestHeaders) {
+      actions.requestData("requestHeaders");
+    }
+
+    if (!responseHeaders) {
+      actions.requestData("responseHeaders");
+    }
+
+    // TODO: Another groups to implement:
+    // 1) Cached Headers
+    // 2) Headers from upload stream
+    let groups = [{
+      key: "responseHeaders",
+      name: Locale.$STR("responseHeaders"),
+      params: responseHeaders
+    }, {
+      key: "requestHeaders",
+      name: Locale.$STR("requestHeaders"),
+      params: requestHeaders
+    }];
+
+    // If response headers are not available yet, display a spinner
+    if (!responseHeaders || !responseHeaders.length) {
+      groups[0].content = Spinner();
+    }
+
+    return (
+      DOM.div({className: "headersTabBox"},
+        DOM.div({className: "panelContent"},
+          NetInfoGroupList({groups: groups})
+        )
+      )
+    );
+  }
+});
+
+// Exports from this module
+module.exports = HeadersTab;
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/net/components/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'cookies-tab.js',
+    'headers-tab.js',
+    'net-info-body.css',
+    'net-info-body.js',
+    'net-info-group-list.js',
+    'net-info-group.css',
+    'net-info-group.js',
+    'net-info-params.css',
+    'net-info-params.js',
+    'params-tab.js',
+    'post-tab.js',
+    'response-tab.css',
+    'response-tab.js',
+    'size-limit.css',
+    'size-limit.js',
+    'spinner.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.css
@@ -0,0 +1,117 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Network Info Body */
+
+.netInfoBody {
+  font-family: var(--net-font-family);
+  font-size: var(--net-font-size);
+  margin: 10px 0 0 0;
+  width: 100%;
+  cursor: default;
+  display: block;
+}
+
+.netInfoBody *:focus {
+  outline: 0 !important;
+}
+
+.netInfoBody .panelContent {
+  word-break: break-all;
+}
+
+/******************************************************************************/
+/* Network Info Body Tabs */
+
+.netInfoBody > .tabs {
+  background-color: transparent;
+  background-image: none;
+  height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-navigation {
+  font-family: var(--net-font-family);
+  font-size: var(--net-font-size);
+  border-bottom-color: var(--net-border);
+  background-color: transparent;
+  text-decoration: none;
+  padding-top: 3px;
+  padding-left: 7px;
+  padding-bottom: 1px;
+  border-bottom: 1px solid var(--net-border);
+}
+
+.netInfoBody > .tabs .tabs-menu {
+  display: table;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+/* This is the trick that makes the tab bottom border invisible */
+.netInfoBody > .tabs .tabs-menu-item {
+  position: relative;
+  bottom: -2px;
+  float: left;
+}
+
+.netInfoBody > .tabs .tabs-menu-item a {
+  display: block;
+  border: 1px solid transparent;
+  text-decoration: none;
+  padding: 5px 8px 4px 8px;;
+  font-weight: bold;
+  color: var(--theme-body-color);
+  border-radius: 4px 4px 0 0;
+}
+
+.netInfoBody > .tabs .tab-panel {
+  background-color: var(--theme-body-background);
+  border: 1px solid transparent;
+  border-top: none;
+  padding: 10px;
+  overflow: auto;
+  height: calc(100% - 31px); /* minus the height of the tab bar */