Merge m-c to b-i
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 15 Mar 2014 21:24:58 -0700
changeset 192041 744d754b68742ebf27c8cd2632ce2d97d08fbabc
parent 192040 e80ce9a854dc6b36463297f2081e5cd3a3066ae2 (current diff)
parent 192033 e3b76b155ca4a68e3333f4c428df9bda20e106e5 (diff)
child 192042 d9ab31c5d5d407aad2cf8766d741999c8c2e33ff
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b-i
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -198,30 +198,32 @@
     <panel id="UITourTooltip"
            type="arrow"
            hidden="true"
            noautofocus="true"
            noautohide="true"
            align="start"
            orient="vertical"
            role="alert">
-      <hbox>
-        <vbox>
+     <vbox>
+      <hbox pack="end">
+        <toolbarbutton id="UITourTooltipClose" class="close-icon"
+                       tooltiptext="&uiTour.infoPanel.close;"/>
+      </hbox>
+      <hbox id="UITourTooltipBody">
+        <vbox id="UITourTooltipIconContainer">
           <image id="UITourTooltipIcon"/>
         </vbox>
         <vbox flex="1">
           <label id="UITourTooltipTitle" flex="1"/>
           <description id="UITourTooltipDescription" flex="1"/>
-          <hbox id="UITourTooltipButtons" flex="1" align="center"/>
-        </vbox>
-        <vbox align="start">
-          <toolbarbutton id="UITourTooltipClose" class="close-icon"
-                         tooltiptext="&uiTour.infoPanel.close;"/>
         </vbox>
       </hbox>
+      <hbox id="UITourTooltipButtons" flex="1" align="center"/>
+     </vbox>
     </panel>
     <!-- type="default" forces frames to be created so that the panel's size can be determined --> 
     <panel id="UITourHighlightContainer"
            type="default"
            hidden="true"
            noautofocus="true"
            noautohide="true"
            flip="none"
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -329,25 +329,27 @@ const DownloadsIndicatorView = {
 
     // No need to show visual notification if the panel is visible.
     if (DownloadsPanel.isPanelShowing) {
       return;
     }
 
     let anchor = DownloadsButton._placeholder;
     let widgetGroup = CustomizableUI.getWidget("downloads-button");
-    let widgetInWindow = widgetGroup.forWindow(window);
-    if (widgetInWindow.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+    let widget = widgetGroup.forWindow(window);
+    if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
       if (anchor && this._isAncestorPanelOpen(anchor)) {
-        // If the containing panel is open, don't do anything:
+        // If the containing panel is open, don't do anything, because the
+        // notification would appear under the open panel. See
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=984023
         return;
       }
 
       // Otherwise, try to use the anchor of the panel:
-      anchor = widgetInWindow.anchor;
+      anchor = widget.anchor;
     }
     if (!anchor || !isElementVisible(anchor.parentNode)) {
       // Our container isn't visible, so can't show the animation:
       return;
     }
 
     if (this._notificationTimeout) {
       clearTimeout(this._notificationTimeout);
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -762,16 +762,22 @@ PlacesViewBase.prototype = {
             break;
         }
         currentChild = currentChild.nextSibling;
       }
       hasMultipleURIs = numURINodes > 1;
     }
 
     if (!hasMultipleURIs) {
+      aPopup.setAttribute("singleitempopup", "true");
+    } else {
+      aPopup.removeAttribute("singleitempopup");
+    }
+
+    if (!hasMultipleURIs) {
       // We don't have to show any option.
       if (aPopup._endOptOpenAllInTabs) {
         aPopup.removeChild(aPopup._endOptOpenAllInTabs);
         aPopup._endOptOpenAllInTabs = null;
 
         aPopup.removeChild(aPopup._endOptSeparator);
         aPopup._endOptSeparator = null;
       }
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -119,21 +119,48 @@ PlacesController.prototype = {
 
     // All other Places Commands are prefixed with "placesCmd_" ... this
     // filters out other commands that we do _not_ support (see 329587).
     const CMD_PREFIX = "placesCmd_";
     return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
   },
 
   isCommandEnabled: function PC_isCommandEnabled(aCommand) {
+    if (PlacesUIUtils.useAsyncTransactions) {
+      switch (aCommand) {
+      case "cmd_cut":
+      case "placesCmd_cut":
+      case "cmd_copy":
+      case "cmd_paste":
+      case "cmd_delete":
+      case "placesCmd_delete":
+      case "placesCmd_moveBookmarks":
+      case "cmd_paste":
+      case "placesCmd_paste":
+      case "placesCmd_new:folder":
+      case "placesCmd_new:livemark":
+      case "placesCmd_new:bookmark":
+      case "placesCmd_new:separator":
+      case "placesCmd_sortBy:name":
+      case "placesCmd_createBookmark":
+        return false;
+      }
+    }
+
     switch (aCommand) {
     case "cmd_undo":
-      return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+      if (!PlacesUIUtils.useAsyncTransactions)
+        return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+
+      return PlacesTransactions.topUndoEntry != null;
     case "cmd_redo":
-      return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+      if (!PlacesUIUtils.useAsyncTransactions)
+        return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+
+      return PlacesTransactions.topRedoEntry != null;
     case "cmd_cut":
     case "placesCmd_cut":
       var nodes = this._view.selectedNodes;
       // If selection includes history nodes there's no reason to allow cut.
       for (var i = 0; i < nodes.length; i++) {
         if (nodes[i].itemId == -1)
           return false;
       }
--- a/browser/components/places/src/PlacesUIUtils.jsm
+++ b/browser/components/places/src/PlacesUIUtils.jsm
@@ -1017,16 +1017,24 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtil
   return PlacesUIUtils.RDF.GetDataSource("rdf:local-store");
 });
 
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
   return Services.prefs.getComplexValue("intl.ellipsis",
                                         Ci.nsIPrefLocalizedString).data;
 });
 
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() {
+  try {
+    return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
+  }
+  catch(ex) { }
+  return false;
+});
+
 XPCOMUtils.defineLazyServiceGetter(this, "URIFixup",
                                    "@mozilla.org/docshell/urifixup;1",
                                    "nsIURIFixup");
 
 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
   const PLACES_STRING_BUNDLE_URI =
     "chrome://browser/locale/places/places.properties";
   return Cc["@mozilla.org/intl/stringbundle;1"].
--- a/browser/devtools/app-manager/content/device.js
+++ b/browser/devtools/app-manager/content/device.js
@@ -1,27 +1,29 @@
 /* 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/. */
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
 
 const {ConnectionManager, Connection}
   = require("devtools/client/connection-manager");
 const {getDeviceFront} = require("devtools/server/actors/device");
 const {getTargetForApp, launchApp, closeApp}
   = require("devtools/app-actor-front");
 const DeviceStore = require("devtools/app-manager/device-store");
 const WebappsStore = require("devtools/app-manager/webapps-store");
 const promise = require("sdk/core/promise");
+const DEFAULT_APP_ICON = "chrome://browser/skin/devtools/app-manager/default-app-icon.png";
 
 window.addEventListener("message", function(event) {
   try {
     let message = JSON.parse(event.data);
     if (message.name == "connection") {
       let cid = parseInt(message.cid);
       for (let c of ConnectionManager.connections) {
         if (c.uid == cid) {
@@ -136,30 +138,55 @@ let UI = {
 
     var tab = document.querySelector(".tab." + name);
     var panel = document.querySelector(".tabpanel." + name);
 
     if (tab) tab.classList.add("selected");
     if (panel) panel.classList.add("selected");
   },
 
-  openToolbox: function(manifest) {
+  openToolboxForApp: function(manifest) {
     if (!this.connected) {
       return;
     }
 
     let app = this.store.object.apps.all.filter(a => a.manifestURL == manifest)[0];
     getTargetForApp(this.connection.client,
                     this.listTabsResponse.webappsActor,
                     manifest).then((target) => {
 
       top.UI.openAndShowToolboxForTarget(target, app.name, app.iconURL);
     }, console.error);
   },
 
+  _getTargetForTab: function (form) {
+      let options = {
+        form: form,
+        client: this.connection.client,
+        chrome: false
+      };
+      let deferred = promise.defer();
+      return devtools.TargetFactory.forRemoteTab(options);
+  },
+
+  openToolboxForTab: function (aNode) {
+    let index = Array.prototype.indexOf.apply(
+      aNode.parentNode.parentNode.parentNode.children,
+      [aNode.parentNode.parentNode]);
+    this.connection.client.listTabs(
+      response => {
+        let tab = response.tabs[index];
+        this._getTargetForTab(tab).then(target => {
+          top.UI.openAndShowToolboxForTarget(
+            target, tab.title, DEFAULT_APP_ICON);
+        }, console.error);
+      }
+    );
+  },
+
   startApp: function(manifest) {
     if (!this.connected) {
       return promise.reject();
     }
     return launchApp(this.connection.client,
                      this.listTabsResponse.webappsActor,
                      manifest);
   },
--- a/browser/devtools/app-manager/content/device.xhtml
+++ b/browser/devtools/app-manager/content/device.xhtml
@@ -39,16 +39,17 @@
           <div id="tabs-headers">
             <div onclick="UI.setTab('apps')" class="tab sidebar-item apps" title="&device.installedAppsTooltip;">&device.installedApps;</div>
             <div onclick="UI.setTab('permissions')" class="tab sidebar-item permissions" title="&device.permissionsTooltip;">
               &device.permissions;
               <a target="_blank" href="&device.permissionsHelpLink;">
                 <button class="help">&device.help;</button>
               </a>
             </div>
+            <div onclick="UI.setTab('browser-tabs')" class="tab sidebar-item browser-tabs" title="&device.browserTabsTooltip;">&device.browserTabs;</div>
           </div>
         </div>
       </aside>
       <section id="detail">
         <div id="tabs">
           <div class="tabpanel apps">
             <div class="app-list" template-loop='{"arrayPath":"apps.all","childSelector":"#app-template"}'></div>
           </div>
@@ -63,37 +64,52 @@
               <section template-loop='{"arrayPath":"device.permissions","childSelector":"#permission-template"}'></section>
             </div>
             <div class="permission-table-footer">
               <div class="allow-label" title="&device.allowTooltip;">&device.allow;</div>
               <div class="prompt-label" title="&device.promptTooltip;">&device.prompt;</div>
               <div class="deny-label" title="&device.denyTooltip;">&device.deny;</div>
             </div>
           </div>
+          <div class="tabpanel browser-tabs">
+            <section template-loop='{"arrayPath":"device.tabs","childSelector":"#browser-tab-template"}'></section>
+          </div>
         </div>
       </section>
     </section>
     <iframe id="connection-footer" hidden="true"></iframe>
   </body>
 
   <template id="permission-template">
   <div class="permission">
     <div template='{"type":"textContent","path":"name"}'></div>
     <div template='{"type":"attribute", "name":"permission", "path":"app"}'></div>
     <div template='{"type":"attribute", "name":"permission", "path":"privileged"}'></div>
     <div template='{"type":"attribute", "name":"permission", "path":"certified"}'></div>
   </div>
   </template>
 
+  <template id="browser-tab-template">
+  <div class="browser-tab">
+    <div class="browser-tab-details">
+      <p template='{"type":"textContent","path":"title"}'></p>
+      <p class="browser-tab-url-subheading" template='{"type":"textContent","path":"url"}'></p>
+    </div>
+    <div class="browser-tab-buttons">
+      <button class="button-debug" template='{"type":"attribute","path":"actor","name":"data-actor"}' onclick="UI.openToolboxForTab(this)" style="display: inline-block;" title="&device.debugBrowserTabTooltip;">&device.debugBrowserTab;</button>
+    </div>
+  </div>
+  </template>
+
   <template id="app-template">
   <div class="app" template='{"type":"attribute","path":"running","name":"running"}'>
     <img class="app-icon" template='{"type":"attribute","path":"iconURL","name":"src"}'></img>
     <span class="app-name" template='{"type":"textContent","path":"name"}'></span>
     <div class="app-buttons">
-      <button class="button-debug" template='{"type":"attribute","path":"manifestURL","name":"data-manifest"}' onclick="UI.openToolbox(this.dataset.manifest)" title="&device.debugAppTooltip;">&device.debugApp;</button>
+      <button class="button-debug" template='{"type":"attribute","path":"manifestURL","name":"data-manifest"}' onclick="UI.openToolboxForApp(this.dataset.manifest)" title="&device.debugAppTooltip;">&device.debugApp;</button>
       <button class="button-start" template='{"type":"attribute","path":"manifestURL","name":"data-manifest"}' onclick="UI.startApp(this.dataset.manifest)" title="&device.startAppTooltip;">&device.startApp;</button>
       <button class="button-stop" template='{"type":"attribute","path":"manifestURL","name":"data-manifest"}' onclick="UI.stopApp(this.dataset.manifest)" title="&device.stopAppTooltip;">&device.stopApp;</button>
     </div>
   </div>
   </template>
 
   <script type="application/javascript;version=1.8" src="utils.js"></script>
   <script type="application/javascript;version=1.8" src="template.js"></script>
--- a/browser/devtools/app-manager/device-store.js
+++ b/browser/devtools/app-manager/device-store.js
@@ -26,16 +26,17 @@ module.exports = DeviceStore = function(
   this._resetStore();
 
   this.destroy = this.destroy.bind(this);
   this._onStatusChanged = this._onStatusChanged.bind(this);
 
   this._connection = connection;
   this._connection.once(Connection.Events.DESTROYED, this.destroy);
   this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+  this._onTabListChanged = this._onTabListChanged.bind(this);
   this._onStatusChanged();
   return this;
 }
 
 DeviceStore.prototype = {
   destroy: function() {
     if (this._connection) {
       // While this.destroy is bound using .once() above, that event may not
@@ -46,29 +47,44 @@ DeviceStore.prototype = {
       _knownDeviceStores.delete(this._connection);
       this._connection = null;
     }
   },
 
   _resetStore: function() {
     this.object.description = {};
     this.object.permissions = [];
+    this.object.tabs = [];
   },
 
   _onStatusChanged: function() {
     if (this._connection.status == Connection.Status.CONNECTED) {
       this._listTabs();
     } else {
       this._resetStore();
     }
   },
 
+  _onTabListChanged: function() {
+    this._listTabs();
+  },
+
   _listTabs: function() {
     this._connection.client.listTabs((resp) => {
+      if (resp.error) {
+        this._connection.disconnect();
+        return;
+      }
       this._deviceFront = getDeviceFront(this._connection.client, resp);
+      // Save remote browser's tabs
+      this.object.tabs = resp.tabs;
+      // Add listener to update remote browser's tabs list in app-manager
+      // when it changes
+      this._connection.client.addListener(
+        'tabListChanged', this._onTabListChanged);
       this._feedStore();
     });
   },
 
   _feedStore: function() {
     this._getDeviceDescription();
     this._getDevicePermissionsTable();
   },
@@ -91,10 +107,10 @@ DeviceStore.prototype = {
           name: name,
           app: permissionsTable[name].app,
           privileged: permissionsTable[name].privileged,
           certified: permissionsTable[name].certified,
         });
       }
       this.object.permissions = permissionsArray;
     });
-  },
+  }
 }
--- a/browser/devtools/app-manager/test/test_device_store.html
+++ b/browser/devtools/app-manager/test/test_device_store.html
@@ -62,23 +62,23 @@ Bug 901520 - [app manager] data store fo
                 }
                 connection.disconnect();
               }).then(null, (error) => ok(false, "Error:" + error));
             });
           });
         });
 
         connection.once("disconnected", function() {
-          compare(store.object, {description:{},permissions:[]}, "empty store after disconnect")
+          compare(store.object, {description:{},permissions:[],tabs:[]}, "empty store after disconnect")
           connection.destroy();
           DebuggerServer.destroy();
           SimpleTest.finish();
         });
 
-        compare(store.object, {description:{},permissions:[]}, "empty store before disconnect")
+        compare(store.object, {description:{},permissions:[],tabs:[]}, "empty store before disconnect")
 
         connection.connect();
 
       }
 
     </script>
   </body>
 </html>
--- a/browser/devtools/layoutview/view.xhtml
+++ b/browser/devtools/layoutview/view.xhtml
@@ -17,17 +17,17 @@
 
     <script type="application/javascript;version=1.8" src="view.js"></script>
 
     <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://browser/skin/devtools/layoutview.css" type="text/css"/>
     <link rel="stylesheet" href="view.css" type="text/css"/>
 
   </head>
-  <body class="theme-body devtools-monospace">
+  <body class="theme-sidebar devtools-monospace">
 
     <p id="header">
       <span id="element-size"></span><span id="element-position"></span>
     </p>
 
     <div id="main">
 
       <div id="margins" data-box="margin" tooltip="&margins.tooltip;">
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -681,17 +681,16 @@ just addresses the organization to follo
 <!ENTITY social.activated.undo.accesskey "U">
 <!ENTITY social.learnMore.label "Learn moreā€¦">
 <!ENTITY social.learnMore.accesskey "l">
 <!ENTITY social.closeNotificationItem.label "Not Now">
 
 
 
 <!ENTITY customizeMode.tabTitle "Customize &brandShortName;">
-<!ENTITY customizeMode.menuAndToolbars.label "Menu and toolbars">
 <!ENTITY customizeMode.menuAndToolbars.header2 "Additional Tools and Features">
 <!ENTITY customizeMode.menuAndToolbars.empty "Want more tools?">
 <!ENTITY customizeMode.menuAndToolbars.emptyLink "Choose from thousands of add-ons">
 <!ENTITY customizeMode.restoreDefaults "Restore Defaults">
 <!ENTITY customizeMode.toolbars "Show / Hide Toolbars">
 <!ENTITY customizeMode.titlebar "Title Bar">
 
 <!ENTITY social.chatBar.commandkey "c">
--- a/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
@@ -27,16 +27,20 @@
 <!ENTITY device.promptTooltip "This permission requires a user prompt for apps of this type">
 <!ENTITY device.deny "Deny">
 <!ENTITY device.denyTooltip "This permission is denied for apps of this type">
 <!ENTITY device.installedApps "Installed Apps">
 <!ENTITY device.installedAppsTooltip "View a list of apps installed on the device. Some apps, such as certified apps, may be excluded from this view.">
 <!ENTITY device.permissions "Permissions">
 <!ENTITY device.permissionsTooltip "View a table of the permissions accessible to the different types of apps">
 <!ENTITY device.permissionsHelpLink "https://developer.mozilla.org/docs/Web/Apps/App_permissions">
+<!ENTITY device.browserTabs "Browser Tabs">
+<!ENTITY device.browserTabsTooltip "View a list of tabs in the browser of the connected device">
+<!ENTITY device.debugBrowserTab "Debug">
+<!ENTITY device.debugBrowserTabTooltip "Open the Developer Tools connected to this browser tab on the device">
 <!ENTITY device.help "Help">
 
 <!ENTITY connection.connectTooltip "Connect to the device">
 <!ENTITY connection.disconnect "Disconnect">
 <!ENTITY connection.disconnectTooltip "Disconnect from the current device or simulator">
 <!ENTITY connection.notConnected2 "Not Connected.">
 <!ENTITY connection.connectTo "Connect to:">
 <!ENTITY connection.noDeviceFound "No device found. Plug a device">
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -548,18 +548,18 @@ this.UITour = {
         tab.removeEventListener("TabBecomingWindow", this);
       }
     }
     this.originTabs.delete(aWindow);
 
     if (!aWindowClosing) {
       this.hideHighlight(aWindow);
       this.hideInfo(aWindow);
-      aWindow.PanelUI.panel.removeAttribute("noautohide");
-      this.recreatePopup(aWindow.PanelUI.panel);
+      // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
+      this.hideMenu(aWindow, "appMenu");
     }
 
     this.endUrlbarCapture(aWindow);
     this.removePinnedTab(aWindow);
     this.resetTheme();
   },
 
   getChromeWindow: function(aContentDocument) {
@@ -907,16 +907,19 @@ this.UITour = {
         let el = document.createElement("button");
         el.setAttribute("label", button.label);
         if (button.iconURL)
           el.setAttribute("image", button.iconURL);
 
         if (button.style == "link")
           el.setAttribute("class", "button-link");
 
+        if (button.style == "primary")
+          el.setAttribute("class", "button-primary");
+
         let callbackID = button.callbackID;
         el.addEventListener("command", event => {
           tooltip.hidePopup();
           this.sendPageCallback(aContentDocument, callbackID);
         });
 
         tooltipButtons.appendChild(el);
       }
--- a/browser/modules/test/browser_UITour.js
+++ b/browser/modules/test/browser_UITour.js
@@ -246,9 +246,15 @@ let tests = [
         is(desc.textContent, "search text", "Popup should have correct description text");
 
         done();
       });
     });
 
     gContentAPI.showInfo("urlbar", "urlbar title", "urlbar text");
   },
+
+  // Make sure this test is last in the file so the appMenu gets left open and done will confirm it got tore down.
+  function cleanupMenus(done) {
+    gContentAPI.showMenu("appMenu");
+    done();
+  },
 ];
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -123,16 +123,19 @@ function UITourTest() {
 
       let highlight = document.getElementById("UITourHighlightContainer");
       is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
 
       let tooltip = document.getElementById("UITourTooltip");
       is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed");
 
       ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
+      ok(!PanelUI.panel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
+      isnot(PanelUI.panel.state, "open", "The panel shouldn't be open");
+      is(document.getElementById("PanelUI-menu-button").hasAttribute("open"), false, "Menu button should know that the menu is closed");
 
       is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
 
       executeSoon(nextTest);
     });
   }
 
   function nextTest() {
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -76,23 +76,23 @@
 }
 
 #urlbar:-moz-lwtheme:not([focused="true"]),
 .searchbar-textbox:-moz-lwtheme:not([focused="true"]) {
   opacity: .85;
 }
 
 /* Places toolbar */
-toolbarbutton.bookmark-item,
+toolbarbutton.bookmark-item:not(.subviewbutton),
 #personal-bookmarks[cui-areatype="toolbar"]:not([overflowedItem=true]) > #bookmarks-toolbar-placeholder {
   margin: 0;
   padding: 2px 3px;
 }
 
-toolbarbutton.bookmark-item:hover:active,
+toolbarbutton.bookmark-item:not(.subviewbutton):hover:active,
 toolbarbutton.bookmark-item[open="true"] {
   padding-top: 3px;
   padding-bottom: 1px;
   -moz-padding-start: 4px;
   -moz-padding-end: 2px;
 }
 
 .bookmark-item > .toolbarbutton-icon,
@@ -183,17 +183,17 @@ toolbarpaletteitem[place="palette"] > #p
 
 /* Bookmark menus */
 menu.bookmark-item,
 menuitem.bookmark-item {
   min-width: 0;
   max-width: 32em;
 }
 
-.bookmark-item > .menu-iconic-left {
+.bookmark-item:not(.subviewbutton) > .menu-iconic-left {
   margin-top: 0;
   margin-bottom: 0;
 }
 
 .bookmark-item > .menu-iconic-left > .menu-iconic-icon {
   -moz-padding-start: 0px;
 }
 
@@ -2230,19 +2230,16 @@ chatbox {
 
 %include ../shared/UITour.inc.css
 
 #UITourTooltipDescription {
   font-size: 1.05rem;
 }
 
 #UITourTooltipClose {
-  -moz-appearance: toolbarbutton;
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-}
-
-#UITourTooltipClose > .toolbarbutton-icon {
-  margin: -4px;
+  -moz-margin-end: -4px;
+  height: 16px;
+  width: 16px;
 }
 
 #UITourTooltipButtons {
-  margin-bottom: 0;
+  margin-bottom: -10px;
 }
--- a/browser/themes/linux/customizableui/panelUIOverlay.css
+++ b/browser/themes/linux/customizableui/panelUIOverlay.css
@@ -1,18 +1,37 @@
 /* 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/. */
 
 %include ../../shared/customizableui/panelUIOverlay.inc.css
 
+.panel-subviews {
+  background-color: -moz-dialog;
+}
+
 #BMB_bookmarksPopup > menuitem[type="checkbox"] {
   -moz-appearance: none !important; /* important, to override toolkit rule */
 }
 
+#BMB_bookmarksPopup menupopup[placespopup=true] {
+  margin-top: -6px;
+  padding-top: 2px;
+}
+
+/* Add some space at the top because there are no headers: */
+#BMB_bookmarksPopup menupopup[placespopup=true] > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox  {
+  padding-top: 4px;
+}
+
+.subviewbutton > .toolbarbutton-text {
+  padding-top: 3px;
+  padding-bottom: 3px;
+}
+
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-appearance: none;
   border: 0;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: 0;
 }
@@ -52,14 +71,26 @@ menu.subviewbutton > .menu-right {
   width: 16px;
   height: 16px;
 }
 
 menu[disabled="true"].subviewbutton > .menu-right {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
+.subviewbutton > .toolbarbutton-icon {
+  -moz-margin-end: 5px !important;
+}
+
+.subviewbutton > .menu-right,
+.subviewbutton > .menu-iconic-left {
+  padding-top: 1px;
+  /* These need !important to override menu.css */
+  margin-top: 1px !important;
+  margin-bottom: 2px !important;
+}
+
 .PanelUI-subView toolbarseparator,
 .PanelUI-subView menuseparator,
 .cui-widget-panelview menuseparator,
 #PanelUI-footer-inner > toolbarseparator {
   -moz-appearance: none !important;
 }
--- a/browser/themes/linux/devtools/layoutview.css
+++ b/browser/themes/linux/devtools/layoutview.css
@@ -1,21 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-.theme-dark .theme-body {
+.theme-dark .theme-sidebar {
   background-image: url(layout-background-grid.png);
 }
 
-.theme-light .theme-body {
-  background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
-}
-
-.theme-body {
+.theme-sidebar {
   color: hsl(210,53%,45%) !important;
   box-sizing: border-box;
 }
 
 #main {
   background-color: white;
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -4358,21 +4358,23 @@ window > chatbox {
   /* There is no OS fullscreen button prior to Lion so the PB indicator can move closer. */
   #main-window[privatebrowsingmode=temporary] {
     background-position: top right 10px;
   }
 }
 
 %include ../shared/UITour.inc.css
 
-#UITourTooltipIcon {
-  -moz-margin-start: 0;
-}
-
 #UITourTooltipDescription {
-  font-size: 1.1rem;
-  line-height: 1.9rem;
+  font-size: 1.18rem;
+  line-height: 2rem;
 }
 
 #UITourTooltipClose {
-  -moz-margin-end: -15px;
-  margin-top: -12px;
-}
+  -moz-margin-end: -10px;
+  margin-top: -14px;
+}
+
+@media (min-resolution: 2dppx) {
+  #UITourTooltipClose > .toolbarbutton-icon {
+    width: 16px;
+  }
+}
--- a/browser/themes/osx/customizableui/panelUIOverlay.css
+++ b/browser/themes/osx/customizableui/panelUIOverlay.css
@@ -93,18 +93,18 @@
 }
 
 .subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon, .bookmark-item)) > .menu-iconic-left {
   display: none;
 }
 
 menu.subviewbutton,
 menuitem.subviewbutton:not(.panel-subview-footer) {
-  padding-top: 5px;
-  padding-bottom: 5px;
+  padding-top: 2px;
+  padding-bottom: 2px;
 }
 
 /* Override OSX-specific toolkit styles for the bookmarks panel */
 menu.subviewbutton > .menu-right {
   -moz-margin-end: 0;
 }
 menu.subviewbutton > .menu-right > image {
   -moz-image-region: rect(0, 9px, 10px, 0);
--- a/browser/themes/osx/devtools/layoutview.css
+++ b/browser/themes/osx/devtools/layoutview.css
@@ -1,21 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-.theme-dark .theme-body {
+.theme-dark .theme-sidebar {
   background-image: url(layout-background-grid.png);
 }
 
-.theme-light .theme-body {
-  background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
-}
-
-.theme-body {
+.theme-sidebar {
   color: hsl(210,53%,45%) !important;
   box-sizing: border-box;
 }
 
 #main {
   background-color: white;
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
--- a/browser/themes/shared/UITour.inc.css
+++ b/browser/themes/shared/UITour.inc.css
@@ -18,87 +18,103 @@
   background-image: radial-gradient(50% 100%, rgba(0,149,220,0.4) 50%, rgba(0,149,220,0.6) 100%);
   border-radius: 40px;
   border: 1px solid white;
   box-shadow: 0 0 3px 0 rgba(0,0,0,0.5);
   min-height: 32px;
   min-width: 32px;
 }
 
+#UITourTooltipBody {
+  -moz-margin-end: 14px;
+}
+
+#UITourTooltipBody > vbox {
+  padding-top: 4px;
+}
+
+#UITourTooltipIconContainer {
+  -moz-margin-start: -16px;
+}
+
 #UITourTooltipIcon {
   width: 48px;
   height: 48px;
-  -moz-margin-start: 8px;
-  -moz-margin-end: 18px;
+  -moz-margin-start: 28px;
+  -moz-margin-end: 28px;
 }
 
 #UITourTooltipTitle,
 #UITourTooltipDescription {
   max-width: 20rem;
 }
 
 #UITourTooltipTitle {
-  font-size: 1.3rem;
+  font-size: 1.45rem;
   font-weight: bold;
   -moz-margin-start: 0;
   -moz-margin-end: 0;
-  margin: 10px 0 19px 0;
+  margin: 0 0 9px 0;
 }
 
 #UITourTooltipDescription {
   -moz-margin-start: 0;
   -moz-margin-end: 0;
   font-size: 1.15rem;
   line-height: 1.8rem;
+  margin-bottom: 0; /* Override global.css */
 }
 
 #UITourTooltipClose {
   -moz-appearance: none;
   border: none;
   background-color: transparent;
   min-width: 0;
   -moz-margin-start: 4px;
-  -moz-margin-end: -10px;
-  margin-top: -8px;
+  margin-top: -2px;
+}
+
+#UITourTooltipClose > .toolbarbutton-text {
+  display: none;
 }
 
 #UITourTooltipButtons {
-  margin: 1.5em 0 .9em 0;
+  -moz-box-pack: end;
+  background-color: hsla(210,4%,10%,.07);
+  border-top: 1px solid hsla(210,4%,10%,.14);
+  margin: 24px -16px -16px;
+  padding: 2em 15px;
 }
 
 #UITourTooltipButtons > button {
-  margin-left: 6px;
-  margin-right: 6px;
+  margin: 0 15px;
 }
 
 #UITourTooltipButtons > button:first-child {
   -moz-margin-start: 0;
 }
 
-#UITourTooltipButtons > button:last-child {
-  -moz-margin-end: 24px;
-}
-
 #UITourTooltipButtons > button[image] > .button-box > .button-icon {
   width: 16px;
   height: 16px;
   -moz-margin-end: 5px;
 }
 
 #UITourTooltipButtons > button .button-text {
   font-size: 1.15rem;
 }
 
 #UITourTooltipButtons > button:not(.button-link) {
   -moz-appearance: none;
-  padding: 2px 10px;
-  background-color: hsla(210,4%,10%,.08);
-  border-radius: 2px;
+  background-color: rgb(251,251,251);
+  border-radius: 3px;
   border: 1px solid;
-  border-color: hsla(210,4%,10%,.1);
+  border-color: rgb(192,192,192);
+  color: rgb(71,71,71);
+  padding: 4px 30px;
   transition-property: background-color, border-color;
   transition-duration: 150ms;
 }
 
 #UITourTooltipButtons > button:not(.button-link):not(:active):hover {
   background-color: hsla(210,4%,10%,.15);
   border-color: hsla(210,4%,10%,.15);
   box-shadow: 0 1px 0 0 hsla(210,4%,10%,.05) inset;
@@ -112,8 +128,20 @@
   color: rgba(0,0,0,0.35);
   padding-left: 10px;
   padding-right: 10px;
 }
 
 #UITourTooltipButtons > button.button-link:hover {
   color: black;
 }
+
+/* The primary button gets the same color as the customize button. */
+#UITourTooltipButtons > button.button-primary {
+  background-color: rgb(116,191,67);
+  color: white;
+  padding-left: 30px;
+  padding-right: 30px;
+}
+
+#UITourTooltipButtons > button.button-primary:not(:active):hover {
+  background-color: rgb(105,173,61);
+}
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -96,17 +96,17 @@
 .panel-subview-body {
   overflow-y: auto;
   overflow-x: hidden;
   -moz-box-flex: 1;
 }
 
 #PanelUI-popup .panel-subview-body {
   margin: -4px;
-  padding: 2px 4px;
+  padding: 4px 4px;
 }
 
 .panel-subview-header,
 .subviewbutton.panel-subview-footer {
   padding: 12px;
 }
 
 .panel-subview-header {
@@ -581,23 +581,21 @@ toolbarpaletteitem[place="palette"] > to
 }
 
 panelview .toolbarbutton-1,
 .subviewbutton,
 .widget-overflow-list .toolbarbutton-1,
 .panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton {
   -moz-appearance: none;
-  padding: 2px 6px;
+  padding: 0 6px;
   background-color: hsla(210,4%,10%,0);
   border-radius: 2px;
   border-style: solid;
   border-color: hsla(210,4%,10%,0);
-  transition-property: background-color, border-color;
-  transition-duration: 150ms;
 }
 
 panelview .toolbarbutton-1,
 .subviewbutton,
 .widget-overflow-list .toolbarbutton-1,
 .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton {
   border-width: 1px;
 }
@@ -610,24 +608,30 @@ panelview .toolbarbutton-1,
 .subviewbutton.panel-subview-footer > .toolbarbutton-text,
 .subviewbutton.panel-subview-footer > .menu-text {
   -moz-padding-start: 0;
   -moz-padding-end: 12px;
   -moz-box-flex: 0;
 }
 
 .subviewbutton:not(.panel-subview-footer) {
-  margin: 2px 0;
+  margin: 0;
 }
 
 .subviewbutton:not(.panel-subview-footer) > .toolbarbutton-text,
 /* Bookmark items need a more specific selector. */
 .PanelUI-subView .subviewbutton:not(.panel-subview-footer) > .menu-text,
 .PanelUI-subView .subviewbutton:not(.panel-subview-footer) > .menu-iconic-text {
-  font-size: 1.1em;
+  font: menu;
+}
+
+/* This is a <label> but it should fit in with the menu font- and colorwise. */
+#PanelUI-characterEncodingView-autodetect-label {
+  font: menu;
+  color: inherit;
 }
 
 .cui-widget-panelview .subviewbutton:not(.panel-subview-footer) {
   margin-left: 4px;
   margin-right: 4px;
 }
 
 panelview .toolbarbutton-1,
@@ -675,17 +679,17 @@ menuitem.subviewbutton@menuStateActive@,
 
 .subviewbutton.panel-subview-footer@buttonStateActive@ {
   background-color: hsla(210,4%,10%,.15);
   border-top: 1px solid hsla(210,4%,10%,.12);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
 #BMB_bookmarksPopup .subviewbutton {
-  font: inherit;
+  font: menu;
   font-weight: normal;
 }
 
 #BMB_bookmarksPopup .subviewbutton:not([disabled="true"]) {
   color: inherit;
 }
 
 #BMB_bookmarksPopup .panel-arrowcontainer > .panel-arrowcontent > .popup-internal-box > .autorepeatbutton-up,
@@ -695,32 +699,42 @@ menuitem.subviewbutton@menuStateActive@,
   margin-bottom: 0;
 }
 
 /* Remove padding on xul:arrowscrollbox to avoid extra padding on footer */
 #BMB_bookmarksPopup arrowscrollbox {
   padding-bottom: 0px;
 }
 
-#BMB_bookmarksPopup menupopup {
-  padding-top: 2px;
-}
-
 #BMB_bookmarksPopup menupopup > .bookmarks-actions-menuseparator {
   /* Hide bottom separator as the styled footer includes a top border serving the same purpose */
   display: none;
 }
 
+/* Popups with only one item don't have a footer */
+menupopup[placespopup=true][singleitempopup=true] > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox,
+/* These popups never have a footer */
+#BMB_bookmarksToolbarPopup > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox,
+#BMB_unsortedBookmarksPopup > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox {
+  /* And so they need some bottom padding: */
+  padding-bottom: 4px;
+}
+
+/* Disabled (empty) item is always alone and never has an icon, so fix its left padding */
+#BMB_bookmarksPopup menupopup[emptyplacesresult] .bookmark-item.subviewbutton {
+  padding-left: 6px;
+}
+
 .PanelUI-subView menuseparator,
 .PanelUI-subView toolbarseparator,
 .cui-widget-panelview menuseparator {
   -moz-appearance: none;
   min-height: 0;
   border-top: 1px solid hsla(210,4%,10%,.15);
-  margin: 2px 0;
+  margin: 6px 0;
   padding: 0;
 }
 
 .PanelUI-subView menuseparator,
 .PanelUI-subView toolbarseparator {
   -moz-margin-start: -5px;
   -moz-margin-end: -4px;
 }
@@ -944,19 +958,19 @@ toolbaritem[overflowedItem=true],
   background-clip: padding-box;
   background-position: center;
   background-repeat: no-repeat;
   background-size: 1px 18px;
   box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
 }
 
 .subviewbutton[checked="true"] {
-  background: url("chrome://global/skin/menu/shared-menu-check.png") top 7px left 7px / 11px 11px no-repeat transparent;
+  background: url("chrome://global/skin/menu/shared-menu-check.png") center left 7px / 11px 11px no-repeat transparent;
 }
 
-.PanelUI-subView .menu-iconic-left {
+.subviewbutton > .menu-iconic-left {
   -moz-appearance: none;
   -moz-margin-end: 3px;
 }
 
-.PanelUI-subView menuitem[checked="true"] > .menu-iconic-left {
+menuitem[checked="true"].subviewbutton > .menu-iconic-left {
   visibility: hidden;
 }
--- a/browser/themes/shared/devtools/app-manager/device.css
+++ b/browser/themes/shared/devtools/app-manager/device.css
@@ -303,53 +303,57 @@ header {
   background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.7));
   color: #FFF;
   text-shadow: 0 1px 2px rgba(0,0,0,0.8);
   padding: 10px;
 }
 
 
 
-/*****************      APPS       *****************/
+/*****************      APPS & BROWSER TABS      *****************/
 
 
 
 
-.apps {
+.apps, .browser-tabs {
   display: flex;
   flex-direction: column;
   overflow: auto;
 }
 
-.app {
+.app, .browser-tab {
   display: flex;
   align-items: center;
   order: 1;
 }
 
-.app-name {
+.app-name, .browser-tab-details {
   flex-grow: 1;
   font-weight: bold;
 }
 
-.app {
+.app, .browser-tab {
   padding: 10px 20px;
   border-bottom: 1px solid #CCC;
 }
 
-.app:hover {
+.app:hover, .browser-tab:hover {
   background-color: #EFEFEF;
 }
 
 .app-icon {
   width: 32px;
   height: 32px;
   margin-right: 10px;
 }
 
+.browser-tab-url-subheading {
+  font-size: 10px;
+}
+
 
 
 /*****************      NOT CONNECTED      *****************/
 
 
 
 body:not(.notconnected) > #notConnectedMessage,
 body.notconnected > #content {
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -323,23 +323,23 @@
 @media not all and (-moz-windows-classic) {
   #titlebar-min {
     -moz-margin-end: 2px;
   }
 }
 
 /* ::::: bookmark buttons ::::: */
 
-toolbarbutton.bookmark-item,
+toolbarbutton.bookmark-item:not(.subviewbutton),
 #personal-bookmarks[cui-areatype="toolbar"]:not([overflowedItem=true]) > #bookmarks-toolbar-placeholder {
   margin: 0;
   padding: 2px 3px;
 }
 
-toolbarbutton.bookmark-item:hover:active:not([disabled="true"]),
+toolbarbutton.bookmark-item:not([disabled="true"]):not(.subviewbutton):hover:active,
 toolbarbutton.bookmark-item[open="true"] {
   padding-top: 3px;
   padding-bottom: 1px;
   -moz-padding-start: 4px;
   -moz-padding-end: 2px;
 }
 
 .bookmark-item > .toolbarbutton-icon,
@@ -415,17 +415,17 @@ toolbarpaletteitem[place="palette"] > #p
 /* ::::: bookmark menus ::::: */
 
 menu.bookmark-item,
 menuitem.bookmark-item {
   min-width: 0;
   max-width: 32em;
 }
 
-.bookmark-item > .menu-iconic-left {
+.bookmark-item:not(.subviewbutton) > .menu-iconic-left {
   margin-top: 0;
   margin-bottom: 0;
 }
 
 .bookmark-item > .menu-iconic-left > .menu-iconic-icon {
   -moz-padding-start: 0px;
 }
 
@@ -2189,16 +2189,20 @@ toolbarbutton.bookmark-item[dragover="tr
   padding: 10px;
 }
 
 #notification-popup .popup-notification-closebutton {
   -moz-margin-end: -14px;
   margin-top: -10px;
 }
 
+#notification-popup .panel-promo-box {
+  margin: 10px -10px -10px;
+}
+
 #notification-popup-box {
   position: relative;
   background-color: #fff;
   background-clip: padding-box;
   padding-left: 3px;
   border-radius: 2.5px 0 0 2.5px;
   border-width: 0 8px 0 0;
   border-style: solid;
@@ -2752,8 +2756,12 @@ chatbox {
 /* End customization mode */
 
 #main-window[privatebrowsingmode=temporary] #private-browsing-indicator {
   width: 40px;
   background: url("chrome://browser/skin/privatebrowsing-indicator.png") no-repeat center center;
 }
 
 %include ../shared/UITour.inc.css
+
+#UITourTooltipButtons {
+  margin: 24px -4px -4px;
+}
--- a/browser/themes/windows/customizableui/panelUIOverlay.css
+++ b/browser/themes/windows/customizableui/panelUIOverlay.css
@@ -11,42 +11,76 @@
 
 #PanelUI-contents #zoom-in-btn {
   padding-left: 12px;
   padding-right: 12px;
 }
 
 /* bookmark panel submenus */
 
-#BMB_bookmarksPopup menupopup {
+#BMB_bookmarksPopup menupopup[placespopup=true] {
   -moz-appearance: none;
   background: transparent;
   border: none;
   padding: 6px;
 }
 
-#BMB_bookmarksPopup menupopup > hbox {
+#BMB_bookmarksPopup menupopup[placespopup=true] > hbox {
   /* emulating chrome://browser/content/places/menu.xml#places-popup-arrow but without the arrow */
   box-shadow: 0 0 4px rgba(0,0,0,0.2);
   background: #FFF;
   border: 1px solid rgba(0,0,0,0.25);
   border-radius: 3.5px;
   margin-top: -4px;
 }
 
+#BMB_bookmarksPopup menupopup {
+  padding-top: 2px;
+}
+
+/* Add some space at the top because there are no headers: */
+#BMB_bookmarksPopup menupopup[placespopup=true] > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox  {
+  padding-top: 4px;
+}
+
 #BMB_bookmarksPopup .menu-text {
   color: #000;
 }
 
+#BMB_bookmarksPopup .subviewbutton[disabled=true] > .menu-text {
+  color: #6d6d6d;
+}
+
 /* bookmark panel separator */
 #BMB_bookmarksPopup menuseparator {
   padding-top: 0;
   padding-bottom: 0;
 }
 
+.subviewbutton > .menu-right,
+.subviewbutton > .menu-iconic-left {
+  padding-top: 1px;
+  margin-top: 1px;
+  margin-bottom: 2px;
+}
+
+/* Disabled empty item looks too small otherwise, because it has no icon. */
+menuitem.subviewbutton[disabled]:not(.menuitem-iconic),
+/* Same for checkbox menu items, whose icons lose size due to -moz-appearance: none: */
+menuitem[type="checkbox"].subviewbutton {
+  /* This is 16px for an icon + 3px for its margins + 1px for its padding +
+   * 2px for its border, see above */
+  min-height: 22px;
+}
+
+.subviewbutton > .toolbarbutton-text {
+  padding-top: 3px;
+  padding-bottom: 3px;
+}
+
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-appearance: none;
   border: 0;
   -moz-margin-start: 3px;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   padding: 0 2px;
--- a/browser/themes/windows/devtools/layoutview.css
+++ b/browser/themes/windows/devtools/layoutview.css
@@ -1,21 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-.theme-dark .theme-body {
+.theme-dark .theme-sidebar {
   background-image: url(layout-background-grid.png);
 }
 
-.theme-light .theme-body {
-  background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
-}
-
-.theme-body {
+.theme-sidebar {
   color: hsl(210,53%,45%) !important;
   box-sizing: border-box;
 }
 
 #main {
   background-color: white;
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -288,17 +288,17 @@ case "$target" in
     android_tools="$android_sdk_root"/tools
     android_platform_tools="$android_sdk_root"/platform-tools
     if test ! -d "$android_platform_tools" ; then
         android_platform_tools="$android_sdk"/tools # SDK Tools < r8
     fi
     # The build tools got moved around to different directories in
     # SDK Tools r22.  Try to locate them.
     android_build_tools=""
-    for suffix in android-4.4 android-4.3 android-4.2.2 19.0.2 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0; do
+    for suffix in android-4.4 android-4.3 android-4.2.2 19.0.3 19.0.2 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0; do
         tools_directory="$android_sdk_root/build-tools/$suffix"
         if test -d "$tools_directory" ; then
             android_build_tools="$tools_directory"
             break
         fi
     done
     if test -z "$android_build_tools" ; then
         android_build_tools="$android_platform_tools" # SDK Tools < r22
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -669,17 +669,17 @@ pref("ui.scrolling.overscroll_snap_limit
 // in 1/1000ths of pixels.
 pref("ui.scrolling.min_scrollable_distance", -1);
 // The axis lock mode for panning behaviour - set between standard, free and sticky
 pref("ui.scrolling.axis_lock_mode", "standard");
 // Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
 pref("ui.scrolling.negate_wheel_scrollY", true);
 // Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
 // auto-detect based on reported hardware values
-pref("ui.scrolling.gamepad_dead_zone", 10);
+pref("ui.scrolling.gamepad_dead_zone", 115);
 
 
 // Enable accessibility mode if platform accessibility is enabled.
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
 // Whether to skip images with empty alt text
--- a/mobile/android/base/prompts/Prompt.java
+++ b/mobile/android/base/prompts/Prompt.java
@@ -387,51 +387,47 @@ public class Prompt implements OnClickLi
      *  or if the dialog can't be created because of invalid JSON.
      */
     private void cancelDialog() {
         JSONObject ret = new JSONObject();
         try {
             ret.put("button", -1);
         } catch(Exception ex) { }
         addInputValues(ret);
-        finishDialog(ret);
+        notifyClosing(ret);
     }
 
     /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
      * is closing.
      */
     private void closeDialog(int which) {
         JSONObject ret = new JSONObject();
         mDialog.dismiss();
 
         addButtonResult(ret, which);
         addListResult(ret, which);
         addInputValues(ret);
 
-        finishDialog(ret);
+        notifyClosing(ret);
     }
 
     /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
      * is closing.
      */
-    public void finishDialog(JSONObject aReturn) {
-        mInputs = null;
-        mButtons = null;
-        mDialog = null;
+    private void notifyClosing(JSONObject aReturn) {
         try {
             aReturn.put("guid", mGuid);
         } catch(JSONException ex) { }
 
         // poke the Gecko thread in case it's waiting for new events
         GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
 
         if (mCallback != null) {
             mCallback.onPromptFinished(aReturn.toString());
         }
-        mGuid = null;
     }
 
     /* Handles parsing the initial JSON sent to show dialogs
      */
     private void processMessage(JSONObject geckoObject) {
         String title = geckoObject.optString("title");
         String text = geckoObject.optString("text");
         mGuid = geckoObject.optString("guid");
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -1,16 +1,20 @@
 package org.mozilla.gecko.tests;
 
 import com.jayway.android.robotium.solo.Condition;
 import com.jayway.android.robotium.solo.Solo;
 
 import org.mozilla.gecko.*;
 import org.mozilla.gecko.GeckoThread.LaunchState;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.ContentUris;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.AssetManager;
@@ -54,19 +58,21 @@ abstract class BaseTest extends Activity
     private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME = TestConstants.ANDROID_PACKAGE_NAME + ".App";
     private static final int VERIFY_URL_TIMEOUT = 2000;
     private static final int MAX_LIST_ATTEMPTS = 3;
     private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
     private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
     public static final int MAX_WAIT_MS = 4500;
     public static final int LONG_PRESS_TIME = 6000;
     private static final int GECKO_READY_WAIT_MS = 180000;
+    public static final int MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS = 90000;
 
     private static Class<Activity> mLauncherActivityClass;
     private Activity mActivity;
+    private int mPreferenceRequestID = 0;
     protected Solo mSolo;
     protected Driver mDriver;
     protected Assert mAsserter;
     protected Actions mActions;
     protected String mBaseUrl;
     protected String mRawBaseUrl;
     private String mLogFile;
     protected String mProfile;
@@ -892,9 +898,68 @@ abstract class BaseTest extends Activity
                 test();
             } catch (Exception e) {
                 mAsserter.ok(false,
                              "Test " + this.getClass().getName() + " threw exception: " + e,
                              "");
             }
         }
     }
+
+    /**
+     * Set the preference and wait for it to change before proceeding with the test.
+     */
+    public void setPreferenceAndWaitForChange(final JSONObject jsonPref) {
+        mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
+
+        // Get the preference name from the json and store it in an array. This array 
+        // will be used later while fetching the preference data.
+        String[] prefNames = new String[1];
+        try {
+            prefNames[0] = jsonPref.getString("name");
+        } catch (JSONException e) {
+            mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e));
+        }
+
+        // Wait for confirmation of the pref change before proceeding with the test.
+        final int ourRequestID = mPreferenceRequestID--;
+        final Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
+        mActions.sendPreferencesGetEvent(ourRequestID, prefNames);
+
+        // Wait until we get the correct "Preferences:Data" event
+        waitForCondition(new Condition() {
+            final long endTime = SystemClock.elapsedRealtime() + MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS;
+
+            @Override
+            public boolean isSatisfied() {
+                try {
+                    long timeout = endTime - SystemClock.elapsedRealtime();
+                    if (timeout < 0) {
+                        timeout = 0;
+                    }
+
+                    JSONObject data = new JSONObject(eventExpecter.blockForEventDataWithTimeout(timeout));
+                    int requestID = data.getInt("requestId");
+                    if (requestID != ourRequestID) {
+                        return false;
+                    }
+
+                    JSONArray preferences = data.getJSONArray("preferences");
+                    mAsserter.is(preferences.length(), 1, "Expecting preference array to have one element");
+                    JSONObject prefs = (JSONObject) preferences.get(0);
+                    mAsserter.is(prefs.getString("name"), jsonPref.getString("name"),
+                            "Expecting returned preference name to be the same as the set name");
+                    mAsserter.is(prefs.getString("type"), jsonPref.getString("type"),
+                            "Expecting returned preference type to be the same as the set type");
+                    mAsserter.is(prefs.get("value"), jsonPref.get("value"),
+                            "Expecting returned preference value to be the same as the set value");
+                    return true;
+                } catch(JSONException e) {
+                    mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e));
+                    // Please the java compiler
+                    return false;
+                }
+            }
+        }, MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS);
+
+        eventExpecter.unregisterListener();
+    }
 }
--- a/mobile/android/base/tests/testAddonManager.java
+++ b/mobile/android/base/tests/testAddonManager.java
@@ -50,33 +50,17 @@ public class testAddonManager extends Pi
         verifyPageTitle("Add-ons");
 
         // Change the AMO URL so we do not try to navigate to a live webpage
         JSONObject jsonPref = new JSONObject();
         try {
             jsonPref.put("name", "extensions.getAddons.browseAddons");
             jsonPref.put("type", "string");
             jsonPref.put("value", getAbsoluteUrl("/robocop/robocop_blank_01.html"));
-            mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
-
-            // Wait for confirmation of the pref change before proceeding with the test.
-            final String[] prefNames = { "extensions.getAddons.browseAddons" };
-            final int ourRequestId = 0x7357;
-            Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
-            mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
-
-            JSONObject data = null;
-            int requestId = -1;
-
-            // Wait until we get the correct "Preferences:Data" event
-            while (requestId != ourRequestId) {
-                data = new JSONObject(eventExpecter.blockForEventData());
-                requestId = data.getInt("requestId");
-            }
-            eventExpecter.unregisterListener();
+            setPreferenceAndWaitForChange(jsonPref);
 
         } catch (Exception ex) { 
             mAsserter.ok(false, "exception in testAddonManager", ex.toString());
         }
 
         // Load AMO page by clicking the AMO icon
         DisplayMetrics dm = new DisplayMetrics();
         getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
--- a/mobile/android/base/tests/testAdobeFlash.java
+++ b/mobile/android/base/tests/testAdobeFlash.java
@@ -26,33 +26,17 @@ public class testAdobeFlash extends Pixe
         }
 
         // Enable plugins
         JSONObject jsonPref = new JSONObject();
         try {
             jsonPref.put("name", "plugin.enable");
             jsonPref.put("type", "string");
             jsonPref.put("value", "1");
-            mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
-
-            // Wait for confirmation of the pref change before proceeding with the test.
-            final String[] prefNames = { "plugin.default.state" };
-            final int ourRequestId = 0x7358;
-            Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
-            mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
-
-            JSONObject data = null;
-            int requestId = -1;
-
-            // Wait until we get the correct "Preferences:Data" event
-            while (requestId != ourRequestId) {
-                data = new JSONObject(eventExpecter.blockForEventData());
-                requestId = data.getInt("requestId");
-            }
-            eventExpecter.unregisterListener();
+            setPreferenceAndWaitForChange(jsonPref);
         } catch (Exception ex) {
             mAsserter.ok(false, "exception in testAdobeFlash", ex.toString());
         }
 
         blockForGeckoReady();
 
         String url = getAbsoluteUrl(StringHelper.ROBOCOP_ADOBE_FLASH_URL);
         PaintedSurface painted = loadAndGetPainted(url);
--- a/mobile/android/base/tests/testDoorHanger.java
+++ b/mobile/android/base/tests/testDoorHanger.java
@@ -72,24 +72,22 @@ public class testDoorHanger extends Base
         addTab(BLANK_URL);
 
         // Make sure doorhanger is hidden
         mAsserter.is(mSolo.searchText(GEO_MESSAGE), false, "Geolocation doorhanger notification is hidden when opening a new tab");
         */
 
 
         boolean offlineAllowedByDefault = true;
+        // Save offline-allow-by-default preferences first
+        final String[] prefNames = { "offline-apps.allow_by_default" };
+        final int ourRequestId = 0x7357;
+        final Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
+        mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
         try {
-            // Save offline-allow-by-default preferences first
-            final String[] prefNames = { "offline-apps.allow_by_default" };
-            final int ourRequestId = 0x7357;
-
-            Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
-            mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
-
             JSONObject data = null;
             int requestId = -1;
 
             // Wait until we get the correct "Preferences:Data" event
             while (requestId != ourRequestId) {
                 data = new JSONObject(eventExpecter.blockForEventData());
                 requestId = data.getInt("requestId");
             }
@@ -101,17 +99,17 @@ public class testDoorHanger extends Base
                 offlineAllowedByDefault = pref.getBoolean("value");
             }
 
             // Turn off offline-allow-by-default
             JSONObject jsonPref = new JSONObject();
             jsonPref.put("name", "offline-apps.allow_by_default");
             jsonPref.put("type", "bool");
             jsonPref.put("value", false);
-            mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
+            setPreferenceAndWaitForChange(jsonPref);
         } catch (JSONException e) {
             mAsserter.ok(false, "exception getting preference", e.toString());
         }
 
         // Load offline storage page
         inputAndLoadUrl(OFFLINE_STORAGE_URL);
         waitForText(OFFLINE_MESSAGE);
 
@@ -129,19 +127,19 @@ public class testDoorHanger extends Base
         mAsserter.is(mSolo.searchText(OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when allowing storage");
         inputAndLoadUrl(OFFLINE_STORAGE_URL);
         mAsserter.is(mSolo.searchText(OFFLINE_MESSAGE), false, "Offline storage doorhanger is no longer triggered");
 
         try {
             // Revert offline setting
             JSONObject jsonPref = new JSONObject();
             jsonPref.put("name", "offline-apps.allow_by_default");
-            jsonPref.put("type", "boolean");
+            jsonPref.put("type", "bool");
             jsonPref.put("value", offlineAllowedByDefault);
-            mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
+            setPreferenceAndWaitForChange(jsonPref);
         } catch (JSONException e) {
             mAsserter.ok(false, "exception setting preference", e.toString());
         }
 
 
         // Load login page
         inputAndLoadUrl(LOGIN_URL);
         waitForText(LOGIN_MESSAGE);
--- a/mobile/android/base/tests/testPasswordEncrypt.java
+++ b/mobile/android/base/tests/testPasswordEncrypt.java
@@ -108,32 +108,17 @@ public class testPasswordEncrypt extends
     }
 
     private void toggleMasterPassword(String passwd) {
         JSONObject jsonPref = new JSONObject();
         try {
             jsonPref.put("name", "privacy.masterpassword.enabled");
             jsonPref.put("type", "string");
             jsonPref.put("value", passwd);
-            mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString());
-
-            // Wait for confirmation of the pref change before proceeding with the test.
-            final String[] prefNames = { "privacy.masterpassword.enabled" };
-            final int ourRequestId = 0x73577;
-            Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
-            mActions.sendPreferencesGetEvent(ourRequestId, prefNames);
-
-            JSONObject data = null;
-            int requestId = -1;
-
-            // Wait until we get the correct "Preferences:Data" event
-            while (requestId != ourRequestId) {
-                data = new JSONObject(eventExpecter.blockForEventData());
-                requestId = data.getInt("requestId");
-            }
+            setPreferenceAndWaitForChange(jsonPref);
         } catch (Exception ex) { 
             mAsserter.ok(false, "exception in toggleMasterPassword", ex.toString());
         }
     }
 
     @Override
     public void tearDown() throws Exception {
         // remove the entire signons.sqlite file
--- a/toolkit/components/osfile/modules/_PromiseWorker.jsm
+++ b/toolkit/components/osfile/modules/_PromiseWorker.jsm
@@ -171,18 +171,28 @@ PromiseWorker.prototype = {
    *
    * @return {promise}
    */
   post: function post(fun, array, closure) {
     let deferred = Promise.defer();
     let id = ++this._id;
     let message = {fun: fun, args: array, id: id};
     this._log("Posting message", message);
+    try {
+      this._worker.postMessage(message);
+    } catch (ex if typeof ex == "number") {
+      this._log("Could not post message", message, "due to xpcom error", ex);
+      // handle raw xpcom errors (see eg bug 961317)
+      return Promise.reject(new Components.Exception("Error in postMessage", ex));
+    } catch (ex) {
+      this._log("Could not post message", message, "due to error", ex);
+      return Promise.reject(ex);
+    }
+
     this._queue.push({deferred:deferred, closure: closure, id: id});
-    this._worker.postMessage(message);
     this._log("Message posted");
     return deferred.promise;
   }
 };
 
 /**
  * An error that has been serialized by the worker.
  *
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -116,27 +116,40 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
  *   [1st undo txn, 2nd undo txn],  <= 1st undo entry
  *   [1st undo txn, 2nd undo txn]   <= 2nd undo entry ]
  * undoPostion: 2.
  *
  * Note that when a new entry is created, all redo entries are removed.
  */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
 
+// Updates commands in the undo group of the active window commands.
+// Inactive windows commands will be updated on focus.
+function updateCommandsOnActiveWindow() {
+  // Updating "undo" will cause a group update including "redo".
+  try {
+    let win = Services.focus.activeWindow;
+    if (win)
+      win.updateCommands("undo");
+  }
+  catch(ex) { console.error(ex, "Couldn't update undo commands"); }
+}
+
 // The internal object for managing the transactions history.
 // The public API is included in PlacesTransactions.
 // TODO bug 982099: extending the array "properly" makes it painful to implement
 // getters.  If/when ES6 gets proper array subclassing we can revise this.
 let TransactionsHistory = [];
 TransactionsHistory.__proto__ = {
   __proto__: Array.prototype,
 
@@ -168,16 +181,17 @@ TransactionsHistory.__proto__ = {
         // undo entry.  Report the error and clear the undo history.
         console.error(ex,
                       "Couldn't undo a transaction, clearing all undo entries.");
         this.clearUndoEntries();
         return;
       }
     }
     this._undoPosition++;
+    updateCommandsOnActiveWindow();
   },
 
   /**
    * Redo the top redo entry, if any, and update the undo position accordingly.
    */
   redo: function* () {
     let entry = this.topRedoEntry;
     if (!entry)
@@ -196,16 +210,17 @@ TransactionsHistory.__proto__ = {
         // redo entry. Report the error and clear the undo history.
         console.error(ex,
                       "Couldn't redo a transaction, clearing all redo entries.");
         this.clearRedoEntries();
         return;
       }
     }
     this._undoPosition--;
+    updateCommandsOnActiveWindow();
   },
 
   /**
    * Add a transaction either as a new entry, if forced or if there are no undo
    * entries, or to the top undo entry.
    *
    * @param aTransaction
    *        the transaction object to be added to the transaction history.
@@ -217,16 +232,17 @@ TransactionsHistory.__proto__ = {
   add: function (aTransaction, aForceNewEntry = false) {
     if (this.length == 0 || aForceNewEntry) {
       this.clearRedoEntries();
       this.unshift([aTransaction]);
     }
     else {
       this[this.undoPosition].unshift(aTransaction);
     }
+    updateCommandsOnActiveWindow();
   },
 
   /**
    * Clear all undo entries.
    */
   clearUndoEntries: function () {
     if (this.undoPosition < this.length)
       this.splice(this.undoPosition);
@@ -406,30 +422,40 @@ let PlacesTransactions = {
    * @param aIndex
    *        the index of the entry to retrieve.
    * @return an array of transaction objects in their undo order (that is,
    * reversely to the order they were executed).
    * @throw if aIndex is invalid (< 0 or >= length).
    * @note the returned array is a clone of the history entry and is not
    * kept in sync with the original entry if it changes.
    */
-  item: function (aIndex) {
+  entry: function (aIndex) {
     if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
       throw new Error("Invalid index");
 
     return TransactionsHistory[aIndex];
   },
 
   /**
    * The index of the top undo entry in the transactions history.
    * If there are no undo entries, it equals to |length|.
    * Entries past this point
    * Entries at and past this point are redo entries.
    */
   get undoPosition() TransactionsHistory.undoPosition,
+
+  /**
+   * Shortcut for accessing the top undo entry in the transaction history.
+   */
+  get topUndoEntry() TransactionsHistory.topUndoEntry,
+
+  /**
+   * Shortcut for accessing the top redo entry in the transaction history.
+   */
+  get topRedoEntry() TransactionsHistory.topRedoEntry
 };
 
 /**
  * Internal helper for defining the standard transactions and their input.
  * It takes the required and optional properties, and generates the public
  * constructor (which takes the input in the form of a plain object) which,
  * when called, creates the argument-less "public" |execute| method by binding
  * the input properties to the function arguments (required properties first,
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -111,28 +111,53 @@ function run_test() {
   bmsvc.addObserver(observer, false);
   do_register_cleanup(function () {
     bmsvc.removeObserver(observer);
   });
 
   run_next_test();
 }
 
-function ensureUndoState(aEntries = [], aUndoPosition = 0) {
-  do_check_eq(PT.length, aEntries.length);
-  do_check_eq(PT.undoPosition, aUndoPosition);
+function sanityCheckTransactionHistory() {
+  do_check_true(PT.undoPosition <= PT.length);
+
+  let check_entry_throws = f => {
+    try {
+      f();
+      do_throw("PT.entry should throw for invalid input");
+    } catch(ex) {}
+  };
+  check_entry_throws( () => PT.entry(-1) );
+  check_entry_throws( () => PT.entry({}) );
+  check_entry_throws( () => PT.entry(PT.length) );
 
-  for (let i = 0; i < aEntries.length; i++) {
-    let testEntry = aEntries[i];
-    let undoEntry = PT.item(i);
-    do_check_eq(testEntry.length, undoEntry.length);
-    for (let j = 0; j < testEntry.length; j++) {
-      do_check_eq(testEntry[j], undoEntry[j]);
-    }
+  if (PT.undoPosition < PT.length)
+    do_check_eq(PT.topUndoEntry, PT.entry(PT.undoPosition));
+  else
+    do_check_null(PT.topUndoEntry);
+  if (PT.undoPosition > 0)
+    do_check_eq(PT.topRedoEntry, PT.entry(PT.undoPosition - 1));
+  else
+    do_check_null(PT.topRedoEntry);
+}
+
+function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) {
+  // ensureUndoState is called in various places during this test, so it's
+  // a good places to sanity-check the transaction-history APIs in all
+  // cases.
+  sanityCheckTransactionHistory();
+
+  do_check_eq(PT.length, aExpectedEntries.length);
+  do_check_eq(PT.undoPosition, aExpectedUndoPosition);
+
+  function checkEqualEntries(aExpectedEntry, aActualEntry) {
+    do_check_eq(aExpectedEntry.length, aActualEntry.length);
+    aExpectedEntry.forEach( (t, i) => do_check_eq(t, aActualEntry[i]) );
   }
+  aExpectedEntries.forEach( (e, i) => checkEqualEntries(e, PT.entry(i)) );
 }
 
 function ensureItemsAdded(...items) {
   do_check_eq(observer.itemsAdded.size, items.length);
   for (let item of items) {
     do_check_true(observer.itemsAdded.has(item.GUID));
     let info = observer.itemsAdded.get(item.GUID);
     do_check_eq(info.parentGUID, item.parentGUID);