Bug 1068284 - UI Tour: Add ability to highlight search provider in search menu. r=MattN
authorBlair McBride <bmcbride@mozilla.com>
Wed, 15 Oct 2014 13:48:49 +1300
changeset 218131 4dc2af2e837c
parent 218130 f4c017d24f92
child 218132 aea178b2ec0c
push id553
push userryanvm@gmail.com
push date2014-10-29 12:21 +0000
treeherdermozilla-release@766b4b4fa7c7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1068284
milestone33.0
Bug 1068284 - UI Tour: Add ability to highlight search provider in search menu. r=MattN
browser/modules/UITour.jsm
browser/modules/test/browser_UITour.js
browser/modules/test/browser_UITour_availableTargets.js
browser/modules/test/uitour.js
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -6,16 +6,17 @@
 
 this.EXPORTED_SYMBOLS = ["UITour"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
   "resource://gre/modules/PermissionsUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
@@ -35,16 +36,19 @@ const BUCKET_TIMESTEPS    = [
   3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
   10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
   60 * 60 * 1000, // Until 1 hour after tab is closed/inactive.
 ];
 
 // Time after which seen Page IDs expire.
 const SEENPAGEID_EXPIRY  = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
 
+// Prefix for any target matching a search engine.
+const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
+
 
 this.UITour = {
   url: null,
   seenPageIDs: null,
   pageIDSourceTabs: new WeakMap(),
   pageIDSourceWindows: new WeakMap(),
   /* Map from browser windows to a set of tabs in which a tour is open */
   originTabs: new WeakMap(),
@@ -370,17 +374,20 @@ this.UITour = {
       }
 
       case "removePinnedTab": {
         this.removePinnedTab(window);
         break;
       }
 
       case "showMenu": {
-        this.showMenu(window, data.name);
+        this.showMenu(window, data.name, () => {
+          if (typeof data.showCallbackID == "string")
+            this.sendPageCallback(contentDocument, data.showCallbackID);
+        });
         break;
       }
 
       case "hideMenu": {
         this.hideMenu(window, data.name);
         break;
       }
 
@@ -683,16 +690,21 @@ this.UITour = {
     if (aTargetName == "pinnedTab") {
       deferred.resolve({
           targetName: aTargetName,
           node: this.ensurePinnedTab(aWindow, aSticky)
       });
       return deferred.promise;
     }
 
+    if (aTargetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
+      let engineID = aTargetName.slice(TARGET_SEARCHENGINE_PREFIX.length);
+      return this.getSearchEngineTarget(aWindow, engineID);
+    }
+
     let targetObject = this.targets.get(aTargetName);
     if (!targetObject) {
       deferred.reject("The specified target name is not in the allowed set");
       return deferred.promise;
     }
 
     let targetQuery = targetObject.query;
     aWindow.PanelUI.ensureReady().then(() => {
@@ -815,35 +827,49 @@ this.UITour = {
   },
 
   /**
    * @param aTarget    The element to highlight.
    * @param aEffect    (optional) The effect to use from UITour.highlightEffects or "none".
    * @see UITour.highlightEffects
    */
   showHighlight: function(aTarget, aEffect = "none") {
-    function showHighlightPanel(aTargetEl) {
-      let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight");
+    let window = aTarget.node.ownerDocument.defaultView;
+
+    function showHighlightPanel() {
+      if (aTarget.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
+        // This won't affect normal higlights done via the panel, so we need to
+        // manually hide those.
+        this.hideHighlight(window);
+        aTarget.node.setAttribute("_moz-menuactive", true);
+        return;
+      }
+
+      // Conversely, highlights for search engines are highlighted via CSS
+      // rather than a panel, so need to be manually removed.
+      this._hideSearchEngineHighlight(window);
+
+      let highlighter = aTarget.node.ownerDocument.getElementById("UITourHighlight");
 
       let effect = aEffect;
       if (effect == "random") {
         // Exclude "random" from the randomly selected effects.
         let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1));
         if (randomEffect == this.highlightEffects.length)
           randomEffect--; // On the order of 1 in 2^62 chance of this happening.
         effect = this.highlightEffects[randomEffect];
       }
       // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays.
       highlighter.setAttribute("active", "none");
-      aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
+      aTarget.node.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
       highlighter.setAttribute("active", effect);
       highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
       highlighter.parentElement.hidden = false;
 
-      let targetRect = aTargetEl.getBoundingClientRect();
+      let targetRect = aTarget.node.getBoundingClientRect();
       let highlightHeight = targetRect.height;
       let highlightWidth = targetRect.width;
       let minDimension = Math.min(highlightHeight, highlightWidth);
       let maxDimension = Math.max(highlightHeight, highlightWidth);
 
       // If the dimensions are within 200% of each other (to include the bookmarks button),
       // make the highlight a circle with the largest dimension as the diameter.
       if (maxDimension / minDimension <= 3.0) {
@@ -857,52 +883,69 @@ this.UITour = {
       highlighter.style.width = highlightWidth + "px";
 
       // Close a previous highlight so we can relocate the panel.
       if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") {
         highlighter.parentElement.hidePopup();
       }
       /* The "overlap" position anchors from the top-left but we want to centre highlights at their
          minimum size. */
-      let highlightWindow = aTargetEl.ownerDocument.defaultView;
+      let highlightWindow = aTarget.node.ownerDocument.defaultView;
       let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement);
       let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
       let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
       let highlightStyle = highlightWindow.getComputedStyle(highlighter);
       let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
       let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
       let offsetX = paddingTopPx
                       - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
       let offsetY = paddingLeftPx
                       - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
-
       this._addAnnotationPanelMutationObserver(highlighter.parentElement);
-      highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
+      highlighter.parentElement.openPopup(aTarget.node, "overlap", offsetX, offsetY);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aTarget.node))
       return;
 
     this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
                                        this.targetIsInAppMenu(aTarget),
-                                       showHighlightPanel.bind(this, aTarget.node));
+                                       showHighlightPanel.bind(this));
   },
 
   hideHighlight: function(aWindow) {
     let tabData = this.pinnedTabs.get(aWindow);
     if (tabData && !tabData.sticky)
       this.removePinnedTab(aWindow);
 
     let highlighter = aWindow.document.getElementById("UITourHighlight");
     this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
     highlighter.parentElement.hidePopup();
     highlighter.removeAttribute("active");
 
     this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
+    this._hideSearchEngineHighlight(aWindow);
+  },
+
+  _hideSearchEngineHighlight: function(aWindow) {
+    // We special case highlighting items in the search engines dropdown,
+    // so just blindly remove any highlight there.
+    let searchMenuBtn = null;
+    try {
+      searchMenuBtn = this.targets.get("searchProvider").query(aWindow.document);
+    } catch (e) { /* This is ok to fail. */ }
+    if (searchMenuBtn) {
+      let searchPopup = aWindow.document
+                               .getAnonymousElementByAttribute(searchMenuBtn,
+                                                               "anonid",
+                                                               "searchbar-popup");
+      for (let menuItem of searchPopup.children)
+        menuItem.removeAttribute("_moz-menuactive");
+    }
   },
 
   /**
    * Show an info panel.
    *
    * @param {Document} aContentDocument
    * @param {Node}     aAnchor
    * @param {String}   [aTitle=""]
@@ -992,16 +1035,21 @@ this.UITour = {
       this._addAnnotationPanelMutationObserver(tooltip);
       tooltip.openPopup(aAnchorEl, alignment);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aAnchor.node))
       return;
 
+    // Due to a platform limitation, we can't anchor a panel to an element in a
+    // <menupopup>. So we can't support showing info panels for search engines.
+    if (aAnchor.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX))
+      return;
+
     this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
                                        this.targetIsInAppMenu(aAnchor),
                                        showInfoPanel.bind(this, aAnchor.node));
   },
 
   hideInfo: function(aWindow) {
     let document = aWindow.document;
 
@@ -1011,25 +1059,25 @@ this.UITour = {
     this._setAppMenuStateForAnnotation(aWindow, "info", false);
 
     let tooltipButtons = document.getElementById("UITourTooltipButtons");
     while (tooltipButtons.firstChild)
       tooltipButtons.firstChild.remove();
   },
 
   showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
-    function openMenuButton(aID) {
-      let menuBtn = aWindow.document.getElementById(aID);
-      if (!menuBtn || !menuBtn.boxObject) {
-        aOpenCallback();
+    function openMenuButton(aMenuBtn) {
+      if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) {
+        if (aOpenCallback)
+          aOpenCallback();
         return;
       }
       if (aOpenCallback)
-        menuBtn.addEventListener("popupshown", onPopupShown);
-      menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
+        aMenuBtn.addEventListener("popupshown", onPopupShown);
+      aMenuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
     }
     function onPopupShown(event) {
       this.removeEventListener("popupshown", onPopupShown);
       aOpenCallback(event);
     }
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.panel.setAttribute("noautohide", "true");
@@ -1039,33 +1087,41 @@ this.UITour = {
       }
       aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations);
       aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations);
       if (aOpenCallback) {
         aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
       }
       aWindow.PanelUI.show();
     } else if (aMenuName == "bookmarks") {
-      openMenuButton("bookmarks-menu-button");
+      let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
+      openMenuButton(menuBtn);
+    } else if (aMenuName == "searchEngines") {
+      this.getTarget(aWindow, "searchProvider").then(target => {
+        openMenuButton(target.node);
+      }).catch(Cu.reportError);
     }
   },
 
   hideMenu: function(aWindow, aMenuName) {
-    function closeMenuButton(aID) {
-      let menuBtn = aWindow.document.getElementById(aID);
-      if (menuBtn && menuBtn.boxObject)
-        menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
+    function closeMenuButton(aMenuBtn) {
+      if (aMenuBtn && aMenuBtn.boxObject)
+        aMenuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
     }
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.panel.removeAttribute("noautohide");
       aWindow.PanelUI.hide();
       this.recreatePopup(aWindow.PanelUI.panel);
     } else if (aMenuName == "bookmarks") {
-      closeMenuButton("bookmarks-menu-button");
+      let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
+      closeMenuButton(menuBtn);
+    } else if (aMenuName == "searchEngines") {
+      let menuBtn = this.targets.get("searchProvider").query(aWindow.document);
+      closeMenuButton(menuBtn);
     }
   },
 
   hidePanelAnnotations: function(aEvent) {
     let win = aEvent.target.ownerDocument.defaultView;
     let annotationElements = new Map([
       // [annotationElement (panel), method to hide the annotation]
       [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
@@ -1150,41 +1206,49 @@ this.UITour = {
         break;
       default:
         Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
   getAvailableTargets: function(aContentDocument, aCallbackID) {
-    let window = this.getChromeWindow(aContentDocument);
-    let data = this.availableTargetsCache.get(window);
-    if (data) {
-      this.sendPageCallback(aContentDocument, aCallbackID, data);
-      return;
-    }
+    Task.spawn(function*() {
+      let window = this.getChromeWindow(aContentDocument);
+      let data = this.availableTargetsCache.get(window);
+      if (data) {
+        this.sendPageCallback(aContentDocument, aCallbackID, data);
+        return;
+      }
 
-    let promises = [];
-    for (let targetName of this.targets.keys()) {
-      promises.push(this.getTarget(window, targetName));
-    }
-    Promise.all(promises).then((targetObjects) => {
+      let promises = [];
+      for (let targetName of this.targets.keys()) {
+        promises.push(this.getTarget(window, targetName));
+      }
+      let targetObjects = yield Promise.all(promises);
+
       let targetNames = [
         "pinnedTab",
       ];
+
       for (let targetObject of targetObjects) {
         if (targetObject.node)
           targetNames.push(targetObject.targetName);
       }
-      let data = {
+
+      targetNames = targetNames.concat(
+        yield this.getAvailableSearchEngineTargets(window)
+      );
+
+      data = {
         targets: targetNames,
       };
       this.availableTargetsCache.set(window, data);
       this.sendPageCallback(aContentDocument, aCallbackID, data);
-    }, (err) => {
+    }.bind(this)).catch(err => {
       Cu.reportError(err);
       this.sendPageCallback(aContentDocument, aCallbackID, {
         targets: [],
       });
     });
   },
 
   addNavBarWidget: function (aTarget, aContentDocument, aCallbackID) {
@@ -1240,11 +1304,60 @@ this.UITour = {
   _annotationMutationCallback: function(aMutations) {
     for (let mutation of aMutations) {
       // Remove both attributes at once and ignore remaining mutations to be proccessed.
       mutation.target.removeAttribute("width");
       mutation.target.removeAttribute("height");
       return;
     }
   },
+
+  getAvailableSearchEngineTargets: function(aWindow) {
+    return new Promise(resolve => {
+      this.getTarget(aWindow, "search").then(searchTarget => {
+        if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
+          return resolve([]);
+
+        Services.search.init(() => {
+          let engines = Services.search.getVisibleEngines();
+          resolve([TARGET_SEARCHENGINE_PREFIX + engine.identifier
+                   for (engine of engines)
+                   if (engine.identifier)]);
+        });
+      }).catch(() => resolve([]));
+    });
+  },
+
+  // We only allow matching based on a search engine's identifier - this gives
+  // us a non-changing ID and guarentees we only match against app-provided
+  // engines.
+  getSearchEngineTarget: function(aWindow, aIdentifier) {
+    return new Promise((resolve, reject) => {
+      Task.spawn(function*() {
+        let searchTarget = yield this.getTarget(aWindow, "search");
+        // We're not supporting having the searchbar in the app-menu, because
+        // popups within popups gets crazy. This restriction should be lifted
+        // once bug 988151 is implemented, as the page can then be responsible
+        // for opening each menu when appropriate.
+        if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
+          return reject("Search engine not available");
+
+        yield Services.search.init();
+
+        let searchPopup = searchTarget.node._popup;
+        for (let engineNode of searchPopup.children) {
+          let engine = engineNode.engine;
+          if (engine && engine.identifier == aIdentifier) {
+            return resolve({
+              targetName: TARGET_SEARCHENGINE_PREFIX + engine.identifier,
+              node: engineNode,
+            });
+          }
+        }
+        reject("Search engine not available");
+      }.bind(this)).catch(() => {
+        reject("Search engine not available");
+      });
+    });
+  }
 };
 
 this.UITour.init();
--- a/browser/modules/test/browser_UITour.js
+++ b/browser/modules/test/browser_UITour.js
@@ -190,16 +190,63 @@ let tests = [
     }
 
     let highlight = document.getElementById("UITourHighlight");
     is_element_hidden(highlight, "Highlight should initially be hidden");
 
     gContentAPI.showHighlight("urlbar");
     waitForElementToBeVisible(highlight, checkDefaultEffect, "Highlight should be shown after showHighlight()");
   },
+  function test_highlight_search_engine(done) {
+    let highlight = document.getElementById("UITourHighlight");
+    gContentAPI.showHighlight("urlbar");
+    waitForElementToBeVisible(highlight, () => {
+
+      gContentAPI.showMenu("searchEngines", function() {
+        let searchbar = document.getElementById("searchbar");
+        isnot(searchbar, null, "Should have found searchbar");
+        let searchPopup = document.getAnonymousElementByAttribute(searchbar,
+                                                                   "anonid",
+                                                                   "searchbar-popup");
+        isnot(searchPopup, null, "Should have found search popup");
+
+        function getEngineNode(identifier) {
+          let engineNode = null;
+          for (let node of searchPopup.children) {
+            if (node.engine.identifier == identifier) {
+              engineNode = node;
+              break;
+            }
+          }
+          isnot(engineNode, null, "Should have found search engine node in popup");
+          return engineNode;
+        }
+        let googleEngineNode = getEngineNode("google");
+        let bingEngineNode = getEngineNode("bing");
+
+        gContentAPI.showHighlight("searchEngine-google");
+        waitForCondition(() => googleEngineNode.getAttribute("_moz-menuactive") == "true", function() {
+          is_element_hidden(highlight, "Highlight panel should be hidden by highlighting search engine");
+
+          gContentAPI.showHighlight("searchEngine-bing");
+          waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") == "true", function() {
+            isnot(googleEngineNode.getAttribute("_moz-menuactive"), "true", "Previous engine should no longer be highlighted");
+
+            gContentAPI.hideHighlight();
+            waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") != "true", function() {
+              gContentAPI.hideMenu("searchEngines");
+              waitForCondition(() => searchPopup.state == "closed", function() {
+                done();
+              }, "Search dropdown should close");
+            }, "Menu item should get attribute removed");
+          }, "Menu item should get attribute to make it look active");
+        });
+      });
+    });
+  },
   function test_highlight_effect_unsupported(done) {
     function checkUnsupportedEffect() {
       is(highlight.getAttribute("active"), "none", "No effect should be used when an unsupported effect is requested");
       done();
     }
 
     let highlight = document.getElementById("UITourHighlight");
     is_element_hidden(highlight, "Highlight should initially be hidden");
--- a/browser/modules/test/browser_UITour_availableTargets.js
+++ b/browser/modules/test/browser_UITour_availableTargets.js
@@ -9,16 +9,23 @@ let gContentWindow;
 
 Components.utils.import("resource:///modules/UITour.jsm");
 
 function test() {
   requestLongerTimeout(2);
   UITourTest();
 }
 
+function searchEngineTargets() {
+  let engines = Services.search.getVisibleEngines();
+  return ["searchEngine-" + engine.identifier
+          for (engine of engines)
+          if (engine.identifier)];
+}
+
 let tests = [
   function test_availableTargets(done) {
     gContentAPI.getConfiguration("availableTargets", (data) => {
       ok_targets(data, [
         "accountStatus",
         "addons",
         "appMenu",
         "backForward",
@@ -27,17 +34,17 @@ let tests = [
         "help",
         "home",
         "pinnedTab",
         "privateWindow",
         "quit",
         "search",
         "searchProvider",
         "urlbar",
-      ]);
+      ].concat(searchEngineTargets()));
       ok(UITour.availableTargetsCache.has(window),
          "Targets should now be cached");
       done();
     });
   },
 
   function test_availableTargets_changeWidgets(done) {
     CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
@@ -53,17 +60,17 @@ let tests = [
         "help",
         "home",
         "pinnedTab",
         "privateWindow",
         "quit",
         "search",
         "searchProvider",
         "urlbar",
-      ]);
+      ].concat(searchEngineTargets()));
       ok(UITour.availableTargetsCache.has(window),
          "Targets should now be cached again");
       CustomizableUI.reset();
       ok(!UITour.availableTargetsCache.has(window),
          "Targets should not be cached after reset");
       done();
     });
   },
--- a/browser/modules/test/uitour.js
+++ b/browser/modules/test/uitour.js
@@ -1,21 +1,19 @@
 /* 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/. */
 
-// Copied from the proposed JS library for Bedrock (ie, www.mozilla.org).
-
 // create namespace
 if (typeof Mozilla == 'undefined') {
 	var Mozilla = {};
 }
 
-(function($) {
-  'use strict';
+;(function($) {
+	'use strict';
 
 	// create namespace
 	if (typeof Mozilla.UITour == 'undefined') {
 		Mozilla.UITour = {};
 	}
 
 	var themeIntervalId = null;
 	function _stopCyclingThemes() {
@@ -55,16 +53,19 @@ if (typeof Mozilla == 'undefined') {
 		}
 		document.addEventListener("mozUITourResponse", listener);
 
 		return id;
 	}
 
 	Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
 
+	Mozilla.UITour.CONFIGNAME_SYNC = "sync";
+	Mozilla.UITour.CONFIGNAME_AVAILABLETARGETS = "availableTargets";
+
 	Mozilla.UITour.registerPageID = function(pageID) {
 		_sendEvent('registerPageID', {
 			pageID: pageID
 		});
 	};
 
 	Mozilla.UITour.showHighlight = function(target, effect) {
 		_sendEvent('showHighlight', {
@@ -81,17 +82,17 @@ if (typeof Mozilla == 'undefined') {
 		var buttonData = [];
 		if (Array.isArray(buttons)) {
 			for (var i = 0; i < buttons.length; i++) {
 				buttonData.push({
 					label: buttons[i].label,
 					icon: buttons[i].icon,
 					style: buttons[i].style,
 					callbackID: _waitForCallback(buttons[i].callback)
-			});
+				});
 			}
 		}
 
 		var closeButtonCallbackID, targetCallbackID;
 		if (options && options.closeButtonCallback)
 			closeButtonCallbackID = _waitForCallback(options.closeButtonCallback);
 		if (options && options.targetCallback)
 			targetCallbackID = _waitForCallback(options.targetCallback);
@@ -151,28 +152,44 @@ if (typeof Mozilla == 'undefined') {
 	Mozilla.UITour.addPinnedTab = function() {
 		_sendEvent('addPinnedTab');
 	};
 
 	Mozilla.UITour.removePinnedTab = function() {
 		_sendEvent('removePinnedTab');
 	};
 
-	Mozilla.UITour.showMenu = function(name) {
+	Mozilla.UITour.showMenu = function(name, callback) {
+		var showCallbackID;
+		if (callback)
+			showCallbackID = _waitForCallback(callback);
+
 		_sendEvent('showMenu', {
-			name: name
+			name: name,
+			showCallbackID: showCallbackID,
 		});
 	};
 
 	Mozilla.UITour.hideMenu = function(name) {
 		_sendEvent('hideMenu', {
 			name: name
 		});
 	};
 
+	Mozilla.UITour.startUrlbarCapture = function(text, url) {
+		_sendEvent('startUrlbarCapture', {
+			text: text,
+			url: url
+		});
+	};
+
+	Mozilla.UITour.endUrlbarCapture = function() {
+		_sendEvent('endUrlbarCapture');
+	};
+
 	Mozilla.UITour.getConfiguration = function(configName, callback) {
 		_sendEvent('getConfiguration', {
 			callbackID: _waitForCallback(callback),
 			configuration: configName,
 		});
 	};
 
 	Mozilla.UITour.showFirefoxAccounts = function() {