Bug 1073238 - Split UITour.jsm into chrome and content parts that communicate via messages. r=MattN
authorTomasz Kołodziejski <tkolodziejski@mozilla.com>
Fri, 24 Oct 2014 17:19:00 +0200
changeset 212447 0121cf3ba5d72182926ee3b7294543d07eda26ce
parent 212446 2548cc70c191af400eb452e7a219187a4e4bab24
child 212448 24a2c61c7ae61b6fbfb61fd42eaf314290d87c28
push id27711
push usercbook@mozilla.com
push dateMon, 27 Oct 2014 14:56:47 +0000
treeherdermozilla-central@20408ad61ce5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1073238
milestone36.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
Bug 1073238 - Split UITour.jsm into chrome and content parts that communicate via messages. r=MattN
browser/base/content/browser.js
browser/base/content/content-UITour.js
browser/base/content/content.js
browser/base/jar.mn
browser/components/nsBrowserGlue.js
browser/modules/UITour.jsm
browser/modules/test/browser_UITour.js
browser/modules/test/browser_UITour2.js
browser/modules/test/browser_UITour3.js
browser/modules/test/browser_UITour_detach_tab.js
browser/modules/test/browser_UITour_registerPageID.js
browser/modules/test/head.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -848,16 +848,17 @@ var gBrowserInit = {
     DOMLinkHandler.init();
     gPageStyleMenu.init();
     LanguageDetectionListener.init();
     BrowserOnClick.init();
     DevEdition.init();
 
     let mm = window.getGroupMessageManager("browsers");
     mm.loadFrameScript("chrome://browser/content/content.js", true);
+    mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
 
     // initialize observers and listeners
     // and give C++ access to gBrowser
     XULBrowserWindow.init();
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/content-UITour.js
@@ -0,0 +1,86 @@
+let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const PREF_TEST_WHITELIST = "browser.uitour.testingOrigins";
+const UITOUR_PERMISSION   = "uitour";
+
+let UITourListener = {
+  handleEvent: function (event) {
+    if (!Services.prefs.getBoolPref("browser.uitour.enabled")) {
+      return;
+    }
+    if (!this.ensureTrustedOrigin()) {
+      return;
+    }
+    addMessageListener("UITour:SendPageCallback", this);
+    sendAsyncMessage("UITour:onPageEvent", {detail: event.detail, type: event.type});
+  },
+
+  isTestingOrigin: function(aURI) {
+    if (Services.prefs.getPrefType(PREF_TEST_WHITELIST) != Services.prefs.PREF_STRING) {
+      return false;
+    }
+
+    // Add any testing origins (comma-seperated) to the whitelist for the session.
+    for (let origin of Services.prefs.getCharPref(PREF_TEST_WHITELIST).split(",")) {
+      try {
+        let testingURI = Services.io.newURI(origin, null, null);
+        if (aURI.prePath == testingURI.prePath) {
+          return true;
+        }
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+    return false;
+  },
+
+  // This function is copied from UITour.jsm.
+  isSafeScheme: function(aURI) {
+    let allowedSchemes = new Set(["https", "about"]);
+    if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
+      allowedSchemes.add("http");
+
+    if (!allowedSchemes.has(aURI.scheme))
+      return false;
+
+    return true;
+  },
+
+  ensureTrustedOrigin: function() {
+    if (content.top != content)
+      return false;
+
+    let uri = content.document.documentURIObject;
+
+    if (uri.schemeIs("chrome"))
+      return true;
+
+    if (!this.isSafeScheme(uri))
+      return false;
+
+    let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
+    if (permission == Services.perms.ALLOW_ACTION)
+      return true;
+
+    return this.isTestingOrigin(uri);
+  },
+
+  receiveMessage: function(aMessage) {
+    switch (aMessage.name) {
+      case "UITour:SendPageCallback":
+        this.sendPageCallback(aMessage.data);
+        break;
+    }
+  },
+
+  sendPageCallback: function (detail) {
+    let doc = content.document;
+    let event = new doc.defaultView.CustomEvent("mozUITourResponse", {
+      bubbles: true,
+      detail: Cu.cloneInto(detail, doc.defaultView)
+    });
+    doc.dispatchEvent(event);
+  }
+};
+
+addEventListener("mozUITour", UITourListener, false, true);
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -20,18 +20,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
   "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "UITour",
-  "resource:///modules/UITour.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
 
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
@@ -117,25 +115,16 @@ if (Services.appinfo.processType == Serv
       sendSyncMessage("contextmenu", { editFlags, spellInfo }, { event });
     }
   }
 
   Cc["@mozilla.org/eventlistenerservice;1"]
     .getService(Ci.nsIEventListenerService)
     .addSystemEventListener(global, "contextmenu", handleContentContextMenu, true);
 
-} else {
-  addEventListener("mozUITour", function(event) {
-    if (!Services.prefs.getBoolPref("browser.uitour.enabled"))
-      return;
-
-    let handled = UITour.onPageEvent(event);
-    if (handled)
-      addEventListener("pagehide", UITour);
-  }, false, true);
 }
 
 let AboutHomeListener = {
   init: function(chromeGlobal) {
     chromeGlobal.addEventListener('AboutHomeLoad', this, false, true);
   },
 
   get isAboutHome() {
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -72,16 +72,17 @@ browser.jar:
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/content.js                    (content/content.js)
+        content/browser/content-UITour.js             (content/content-UITour.js)
         content/browser/defaultthemes/1.footer.jpg    (content/defaultthemes/1.footer.jpg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
         content/browser/defaultthemes/2.footer.jpg    (content/defaultthemes/2.footer.jpg)
         content/browser/defaultthemes/2.header.jpg    (content/defaultthemes/2.header.jpg)
         content/browser/defaultthemes/2.icon.jpg      (content/defaultthemes/2.icon.jpg)
         content/browser/defaultthemes/2.preview.jpg   (content/defaultthemes/2.preview.jpg)
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -12,16 +12,19 @@ const XULNS = "http://www.mozilla.org/ke
 const POLARIS_ENABLED = "browser.polaris.enabled";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "UITour",
+                                  "resource:///modules/UITour.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
                                   "resource:///modules/ContentClick.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
                                   "resource:///modules/DirectoryLinksProvider.jsm");
@@ -2548,8 +2551,17 @@ let E10SUINotification = {
 
     win.PopupNotifications.show(browser, "a11y_enabled_with_e10s", promptMessage, null, mainAction, secondaryActions, options);
   },
 };
 #endif
 
 var components = [BrowserGlue, ContentPermissionPrompt];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
+
+
+// Listen for UITour messages.
+// Do it here instead of the UITour module itself so that the UITour module is lazy loaded
+// when the first message is received.
+let globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+globalMM.addMessageListener("UITour:onPageEvent", function(aMessage) {
+  UITour.onPageEvent(aMessage, aMessage.data);
+});
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -20,18 +20,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 
 
-const UITOUR_PERMISSION   = "uitour";
-const PREF_TEST_WHITELIST = "browser.uitour.testingOrigins";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
 const MAX_BUTTONS         = 4;
 
 const BUCKET_NAME         = "UITour";
 const BUCKET_TIMESTEPS    = [
   1 * 60 * 1000, // Until 1 minute after tab is closed/inactive.
   3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
   10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
@@ -217,54 +215,51 @@ this.UITour = {
       Services.prefs.clearUserPref(PREF_SEENPAGEIDS);
       return;
     }
 
     Services.prefs.setCharPref(PREF_SEENPAGEIDS,
                                JSON.stringify([...this.seenPageIDs]));
   },
 
-  onPageEvent: function(aEvent) {
+  onPageEvent: function(aMessage, aEvent) {
     let contentDocument = null;
-    if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
-      contentDocument = aEvent.target;
-    else if (aEvent.target instanceof Ci.nsIDOMHTMLElement)
-      contentDocument = aEvent.target.ownerDocument;
-    else
-      return false;
 
-    // Ignore events if they're not from a trusted origin.
-    if (!this.ensureTrustedOrigin(contentDocument))
-      return false;
+    let browser = aMessage.target;
+    let window = browser.ownerDocument.defaultView;
+    let tab = window.gBrowser.getTabForBrowser(browser);
+    let messageManager = browser.messageManager;
 
     if (typeof aEvent.detail != "object")
       return false;
 
     let action = aEvent.detail.action;
     if (typeof action != "string" || !action)
       return false;
 
     let data = aEvent.detail.data;
     if (typeof data != "object")
       return false;
 
-    let window = this.getChromeWindow(contentDocument);
     // Do this before bailing if there's no tab, so later we can pick up the pieces:
     window.gBrowser.tabContainer.addEventListener("TabSelect", this);
-    let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
-    if (!tab) {
-      // This should only happen while detaching a tab:
-      if (this._detachingTab) {
-        this._queuedEvents.push(aEvent);
-        this._pendingDoc = Cu.getWeakReference(contentDocument);
+
+    if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
+      contentDocument = browser.contentWindow.document;
+      if (!tab) {
+        // This should only happen while detaching a tab:
+        if (this._detachingTab) {
+          this._queuedEvents.push(aEvent);
+          this._pendingDoc = Cu.getWeakReference(contentDocument);
+          return;
+        }
+        Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
+                       "This shouldn't happen!");
         return;
       }
-      Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
-                     "This shouldn't happen!");
-      return;
     }
 
     switch (action) {
       case "registerPageID": {
         // This is only relevant if Telemtry is enabled.
         if (!UITelemetry.enabled)
           break;
 
@@ -310,31 +305,31 @@ this.UITour = {
         targetPromise.then(target => {
           if (!target.node) {
             Cu.reportError("UITour: Target could not be resolved: " + data.target);
             return;
           }
 
           let iconURL = null;
           if (typeof data.icon == "string")
-            iconURL = this.resolveURL(contentDocument, data.icon);
+            iconURL = this.resolveURL(browser, data.icon);
 
           let buttons = [];
           if (Array.isArray(data.buttons) && data.buttons.length > 0) {
             for (let buttonData of data.buttons) {
               if (typeof buttonData == "object" &&
                   typeof buttonData.label == "string" &&
                   typeof buttonData.callbackID == "string") {
                 let button = {
                   label: buttonData.label,
                   callbackID: buttonData.callbackID,
                 };
 
                 if (typeof buttonData.icon == "string")
-                  button.iconURL = this.resolveURL(contentDocument, buttonData.icon);
+                  button.iconURL = this.resolveURL(browser, buttonData.icon);
 
                 if (typeof buttonData.style == "string")
                   button.style = buttonData.style;
 
                 buttons.push(button);
 
                 if (buttons.length == MAX_BUTTONS)
                   break;
@@ -344,17 +339,17 @@ this.UITour = {
 
           let infoOptions = {};
 
           if (typeof data.closeButtonCallbackID == "string")
             infoOptions.closeButtonCallbackID = data.closeButtonCallbackID;
           if (typeof data.targetCallbackID == "string")
             infoOptions.targetCallbackID = data.targetCallbackID;
 
-          this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions);
+          this.showInfo(messageManager, target, data.title, data.text, iconURL, buttons, infoOptions);
         }).then(null, Cu.reportError);
         break;
       }
 
       case "hideInfo": {
         this.hideInfo(window);
         break;
       }
@@ -377,17 +372,17 @@ this.UITour = {
       case "removePinnedTab": {
         this.removePinnedTab(window);
         break;
       }
 
       case "showMenu": {
         this.showMenu(window, data.name, () => {
           if (typeof data.showCallbackID == "string")
-            this.sendPageCallback(contentDocument, data.showCallbackID);
+            this.sendPageCallback(messageManager, data.showCallbackID);
         });
         break;
       }
 
       case "hideMenu": {
         this.hideMenu(window, data.name);
         break;
       }
@@ -423,17 +418,17 @@ this.UITour = {
         break;
       }
 
       case "getConfiguration": {
         if (typeof data.configuration != "string") {
           return false;
         }
 
-        this.getConfiguration(contentDocument, data.configuration, data.callbackID);
+        this.getConfiguration(messageManager, window, data.configuration, data.callbackID);
         break;
       }
 
       case "showFirefoxAccounts": {
         // 'signup' is the only action that makes sense currently, so we don't
         // accept arbitrary actions just to be safe...
         // We want to replace the current tab.
         contentDocument.location.href = "about:accounts?action=signup&entrypoint=uitour";
@@ -445,29 +440,32 @@ this.UITour = {
         ResetProfile.openConfirmationDialog(window);
         break;
       }
 
       case "addNavBarWidget": {
         // Add a widget to the toolbar
         let targetPromise = this.getTarget(window, data.name);
         targetPromise.then(target => {
-          this.addNavBarWidget(target, contentDocument, data.callbackID);
+          this.addNavBarWidget(target, messageManager, data.callbackID);
         }).then(null, Cu.reportError);
         break;
       }
     }
 
-    if (!this.originTabs.has(window))
-      this.originTabs.set(window, new Set());
+    if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
+      if (!this.originTabs.has(window)) {
+        this.originTabs.set(window, new Set());
+      }
 
-    this.originTabs.get(window).add(tab);
-    tab.addEventListener("TabClose", this);
-    tab.addEventListener("TabBecomingWindow", this);
-    window.addEventListener("SSWindowClosing", this);
+      this.originTabs.get(window).add(tab);
+      tab.addEventListener("TabClose", this);
+      tab.addEventListener("TabBecomingWindow", this);
+      window.addEventListener("SSWindowClosing", this);
+    }
 
     return true;
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "pagehide": {
         let window = this.getChromeWindow(aEvent.target);
@@ -616,88 +614,44 @@ this.UITour = {
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShellTreeItem)
                            .rootTreeItem
                            .QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindow)
                            .wrappedJSObject;
   },
 
-  isTestingOrigin: function(aURI) {
-    if (Services.prefs.getPrefType(PREF_TEST_WHITELIST) != Services.prefs.PREF_STRING) {
-      return false;
-    }
-
-    // Add any testing origins (comma-seperated) to the whitelist for the session.
-    for (let origin of Services.prefs.getCharPref(PREF_TEST_WHITELIST).split(",")) {
-      try {
-        let testingURI = Services.io.newURI(origin, null, null);
-        if (aURI.prePath == testingURI.prePath) {
-          return true;
-        }
-      } catch (ex) {
-        Cu.reportError(ex);
-      }
-    }
-    return false;
-  },
-
-  ensureTrustedOrigin: function(aDocument) {
-    if (aDocument.defaultView.top != aDocument.defaultView)
-      return false;
-
-    let uri = aDocument.documentURIObject;
-
-    if (uri.schemeIs("chrome"))
-      return true;
-
-    if (!this.isSafeScheme(uri))
-      return false;
-
-    let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
-    if (permission == Services.perms.ALLOW_ACTION)
-      return true;
-
-    return this.isTestingOrigin(uri);
-  },
-
+  // This function is copied to UITourListener.
   isSafeScheme: function(aURI) {
     let allowedSchemes = new Set(["https", "about"]);
     if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
       allowedSchemes.add("http");
 
     if (!allowedSchemes.has(aURI.scheme))
       return false;
 
     return true;
   },
 
-  resolveURL: function(aDocument, aURL) {
+  resolveURL: function(aBrowser, aURL) {
     try {
-      let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject);
+      let uri = Services.io.newURI(aURL, null, aBrowser.currentURI);
 
       if (!this.isSafeScheme(uri))
         return null;
 
       return uri.spec;
     } catch (e) {}
 
     return null;
   },
 
-  sendPageCallback: function(aDocument, aCallbackID, aData = {}) {
-
+  sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) {
     let detail = {data: aData, callbackID: aCallbackID};
-    detail = Cu.cloneInto(detail, aDocument.defaultView);
-    let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", {
-      bubbles: true,
-      detail: detail
-    });
-
-    aDocument.dispatchEvent(event);
+    aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail);
   },
 
   isElementVisible: function(aElement) {
     let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
     return (targetStyle.display != "none" && targetStyle.visibility == "visible");
   },
 
   getTarget: function(aWindow, aTargetName, aSticky = false) {
@@ -961,26 +915,26 @@ this.UITour = {
       for (let menuItem of searchPopup.children)
         menuItem.removeAttribute("_moz-menuactive");
     }
   },
 
   /**
    * Show an info panel.
    *
-   * @param {Document} aContentDocument
+   * @param {nsIMessageSender} aMessageManager
    * @param {Node}     aAnchor
    * @param {String}   [aTitle=""]
    * @param {String}   [aDescription=""]
    * @param {String}   [aIconURL=""]
    * @param {Object[]} [aButtons=[]]
    * @param {Object}   [aOptions={}]
    * @param {String}   [aOptions.closeButtonCallbackID]
    */
-  showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
+  showInfo: function(aMessageManager, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
                      aButtons = [], aOptions = {}) {
     function showInfoPanel(aAnchorEl) {
       aAnchorEl.focus();
 
       let document = aAnchorEl.ownerDocument;
       let tooltip = document.getElementById("UITourTooltip");
       let tooltipTitle = document.getElementById("UITourTooltipTitle");
       let tooltipDesc = document.getElementById("UITourTooltipDescription");
@@ -1009,38 +963,38 @@ this.UITour = {
           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);
+          this.sendPageCallback(aMessageManager, callbackID);
         });
 
         tooltipButtons.appendChild(el);
       }
 
       tooltipButtons.hidden = !aButtons.length;
 
       let tooltipClose = document.getElementById("UITourTooltipClose");
       let closeButtonCallback = (event) => {
         this.hideInfo(document.defaultView);
         if (aOptions && aOptions.closeButtonCallbackID)
-          this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID);
+          this.sendPageCallback(aMessageManager, aOptions.closeButtonCallbackID);
       };
       tooltipClose.addEventListener("command", closeButtonCallback);
 
       let targetCallback = (event) => {
         let details = {
           target: aAnchor.targetName,
           type: event.type,
         };
-        this.sendPageCallback(aContentDocument, aOptions.targetCallbackID, details);
+        this.sendPageCallback(aMessageManager, aOptions.targetCallbackID, details);
       };
       if (aOptions.targetCallbackID && aAnchor.addTargetListener) {
         aAnchor.addTargetListener(document, targetCallback);
       }
 
       tooltip.addEventListener("popuphiding", function tooltipHiding(event) {
         tooltip.removeEventListener("popuphiding", tooltipHiding);
         tooltipClose.removeEventListener("command", closeButtonCallback);
@@ -1209,44 +1163,44 @@ this.UITour = {
 
     let tab = aWindow.gBrowser.addTab(url, {
       owner: aWindow.gBrowser.selectedTab,
       relatedToCurrent: true
     });
     aWindow.gBrowser.selectedTab = tab;
   },
 
-  getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) {
+  getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) {
     switch (aConfiguration) {
       case "availableTargets":
-        this.getAvailableTargets(aContentDocument, aCallbackID);
+        this.getAvailableTargets(aMessageManager, aWindow, aCallbackID);
         break;
       case "sync":
-        this.sendPageCallback(aContentDocument, aCallbackID, {
+        this.sendPageCallback(aMessageManager, aCallbackID, {
           setup: Services.prefs.prefHasUserValue("services.sync.username"),
         });
         break;
       case "appinfo":
         let props = ["defaultUpdateChannel", "version"];
         let appinfo = {};
         props.forEach(property => appinfo[property] = Services.appinfo[property]);
-        this.sendPageCallback(aContentDocument, aCallbackID, appinfo);
+        this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
         break;
       default:
         Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
-  getAvailableTargets: function(aContentDocument, aCallbackID) {
+  getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
     Task.spawn(function*() {
-      let window = this.getChromeWindow(aContentDocument);
+      let window = aChromeWindow;
       let data = this.availableTargetsCache.get(window);
       if (data) {
-        this.sendPageCallback(aContentDocument, aCallbackID, data);
+        this.sendPageCallback(aMessageManager, aCallbackID, data);
         return;
       }
 
       let promises = [];
       for (let targetName of this.targets.keys()) {
         promises.push(this.getTarget(window, targetName));
       }
       let targetObjects = yield Promise.all(promises);
@@ -1263,41 +1217,41 @@ this.UITour = {
       targetNames = targetNames.concat(
         yield this.getAvailableSearchEngineTargets(window)
       );
 
       data = {
         targets: targetNames,
       };
       this.availableTargetsCache.set(window, data);
-      this.sendPageCallback(aContentDocument, aCallbackID, data);
+      this.sendPageCallback(aMessageManager, aCallbackID, data);
     }.bind(this)).catch(err => {
       Cu.reportError(err);
-      this.sendPageCallback(aContentDocument, aCallbackID, {
+      this.sendPageCallback(aMessageManager, aCallbackID, {
         targets: [],
       });
     });
   },
 
-  addNavBarWidget: function (aTarget, aContentDocument, aCallbackID) {
+  addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) {
     if (aTarget.node) {
       Cu.reportError("UITour: can't add a widget already present: " + data.target);
       return;
     }
     if (!aTarget.allowAdd) {
       Cu.reportError("UITour: not allowed to add this widget: " + data.target);
       return;
     }
     if (!aTarget.widgetName) {
       Cu.reportError("UITour: can't add a widget without a widgetName property: " + data.target);
       return;
     }
 
     CustomizableUI.addWidgetToArea(aTarget.widgetName, CustomizableUI.AREA_NAVBAR);
-    this.sendPageCallback(aContentDocument, aCallbackID);
+    this.sendPageCallback(aMessageManager, aCallbackID);
   },
 
   _addAnnotationPanelMutationObserver: function(aPanelEl) {
 #ifdef XP_LINUX
     let observer = this._annotationPanelMutationObservers.get(aPanelEl);
     if (observer) {
       return;
     }
--- a/browser/modules/test/browser_UITour.js
+++ b/browser/modules/test/browser_UITour.js
@@ -76,22 +76,26 @@ let tests = [
 
     Services.prefs.setBoolPref("browser.uitour.enabled", true);
     done();
   },
   function test_highlight(done) {
     function test_highlight_2() {
       let highlight = document.getElementById("UITourHighlight");
       gContentAPI.hideHighlight();
+
+      waitForElementToBeHidden(highlight, test_highlight_3, "Highlight should be hidden after hideHighlight()");
+    }
+    function test_highlight_3() {
       is_element_hidden(highlight, "Highlight should be hidden after hideHighlight()");
 
       gContentAPI.showHighlight("urlbar");
-      waitForElementToBeVisible(highlight, test_highlight_3, "Highlight should be shown after showHighlight()");
+      waitForElementToBeVisible(highlight, test_highlight_4, "Highlight should be shown after showHighlight()");
     }
-    function test_highlight_3() {
+    function test_highlight_4() {
       let highlight = document.getElementById("UITourHighlight");
       gContentAPI.showHighlight("backForward");
       waitForElementToBeVisible(highlight, done, "Highlight should be shown after showHighlight()");
     }
 
     let highlight = document.getElementById("UITourHighlight");
     is_element_hidden(highlight, "Highlight should initially be hidden");
 
@@ -297,43 +301,37 @@ let tests = [
         gContentAPI.showInfo("urlbar", "test title", "test text");
 
       });
       gContentAPI.hideInfo();
     });
 
     gContentAPI.showInfo("urlbar", "test title", "test text");
   },
-  function test_info_2(done) {
+  taskify(function* test_info_2() {
     let popup = document.getElementById("UITourTooltip");
     let title = document.getElementById("UITourTooltipTitle");
     let desc = document.getElementById("UITourTooltipDescription");
     let icon = document.getElementById("UITourTooltipIcon");
     let buttons = document.getElementById("UITourTooltipButtons");
 
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
-      is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
-      is(title.textContent, "urlbar title", "Popup should have correct title");
-      is(desc.textContent, "urlbar text", "Popup should have correct description text");
-      is(icon.src, "", "Popup should have no icon");
-      is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+    yield showInfoPromise("urlbar", "urlbar title", "urlbar text");
 
-      gContentAPI.showInfo("search", "search title", "search text");
-      executeSoon(function() {
-        is(popup.popupBoxObject.anchorNode, document.getElementById("searchbar"), "Popup should be anchored to the searchbar");
-        is(title.textContent, "search title", "Popup should have correct title");
-        is(desc.textContent, "search text", "Popup should have correct description text");
+    is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
+    is(title.textContent, "urlbar title", "Popup should have correct title");
+    is(desc.textContent, "urlbar text", "Popup should have correct description text");
+    is(icon.src, "", "Popup should have no icon");
+    is(buttons.hasChildNodes(), false, "Popup should have no buttons");
 
-        done();
-      });
-    });
+    yield showInfoPromise("search", "search title", "search text");
 
-    gContentAPI.showInfo("urlbar", "urlbar title", "urlbar text");
-  },
+    is(popup.popupBoxObject.anchorNode, document.getElementById("searchbar"), "Popup should be anchored to the searchbar");
+    is(title.textContent, "search title", "Popup should have correct title");
+    is(desc.textContent, "search text", "Popup should have correct description text");
+  }),
   function test_getConfigurationVersion(done) {
     function callback(result) {
       let props = ["defaultUpdateChannel", "version"];
       for (let property of props) {
         ok(typeof(result[property]) !== undefined, "Check " + property + " isn't undefined.");
         is(result[property], Services.appinfo[property], "Should have the same " + property + " property.");
       }
       done();
@@ -363,13 +361,14 @@ let tests = [
           CustomizableUI.removeWidgetFromArea("panic-button");
           done();
         });
       });
     });
   },
 
   // 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) {
+  taskify(function* cleanupMenus() {
+    let shownPromise = promisePanelShown(window);
     gContentAPI.showMenu("appMenu");
-    done();
-  },
+    yield shownPromise;
+  }),
 ];
--- a/browser/modules/test/browser_UITour2.js
+++ b/browser/modules/test/browser_UITour2.js
@@ -51,53 +51,57 @@ let tests = [
           ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should still be set");
 
           // Move the info outside which shouldn't close the app menu since it was manually opened.
           gContentAPI.showInfo("appMenu", "Open Me", "You know you want to");
           UITour.getTarget(window, "appMenu").then((target) => {
             waitForPopupAtAnchor(popup, target.node, function checkMenuIsStillOpen() {
               isnot(PanelUI.panel.state, "closed",
                     "Menu should remain open since UITour didn't open it in the first place");
+              waitForElementToBeHidden(window.PanelUI.panel, () => {
+                ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
+                done();
+              });
               gContentAPI.hideMenu("appMenu");
-              ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
-              done();
             }, "Info should move to the appMenu button");
           });
         }, "Info should be shown after showInfo() for fixed menu panel items");
       });
     }).then(null, Components.utils.reportError);
   },
-  function test_pinnedTab(done) {
+  taskify(function* test_pinnedTab() {
     is(UITour.pinnedTabs.get(window), null, "Should not already have a pinned tab");
 
-    gContentAPI.addPinnedTab();
+    yield addPinnedTabPromise();
+
     let tabInfo = UITour.pinnedTabs.get(window);
     isnot(tabInfo, null, "Should have recorded data about a pinned tab after addPinnedTab()");
     isnot(tabInfo.tab, null, "Should have added a pinned tab after addPinnedTab()");
     is(tabInfo.tab.pinned, true, "Tab should be marked as pinned");
 
     let tab = tabInfo.tab;
 
-    gContentAPI.removePinnedTab();
+    yield removePinnedTabPromise();
     isnot(gBrowser.tabs[0], tab, "First tab should not be the pinned tab");
     tabInfo = UITour.pinnedTabs.get(window);
     is(tabInfo, null, "Should not have any data about the removed pinned tab after removePinnedTab()");
 
-    gContentAPI.addPinnedTab();
-    gContentAPI.addPinnedTab();
-    gContentAPI.addPinnedTab();
+    yield addPinnedTabPromise();
+    yield addPinnedTabPromise();
+    yield addPinnedTabPromise();
     is(gBrowser.tabs[1].pinned, false, "After multiple calls of addPinnedTab, should still only have one pinned tab");
+  }),
+  taskify(function* test_menu() {
+    let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
 
-    done();
-  },
-  function test_menu(done) {
-    let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
     ise(bookmarksMenuButton.open, false, "Menu should initially be closed");
+    gContentAPI.showMenu("bookmarks");
 
-    gContentAPI.showMenu("bookmarks");
-    ise(bookmarksMenuButton.open, true, "Menu should be shown after showMenu()");
+    yield waitForConditionPromise(() => {
+      return bookmarksMenuButton.open;
+    }, "Menu should be visible after showMenu()");
 
     gContentAPI.hideMenu("bookmarks");
-    ise(bookmarksMenuButton.open, false, "Menu should be closed after hideMenu()");
-
-    done();
-  },
+    yield waitForConditionPromise(() => {
+        return !bookmarksMenuButton.open;
+    }, "Menu should be hidden after hideMenu()");
+  }),
 ];
--- a/browser/modules/test/browser_UITour3.js
+++ b/browser/modules/test/browser_UITour3.js
@@ -11,155 +11,138 @@ Components.utils.import("resource:///mod
 
 requestLongerTimeout(2);
 
 function test() {
   UITourTest();
 }
 
 let tests = [
-  function test_info_icon(done) {
+  taskify(function* test_info_icon() {
     let popup = document.getElementById("UITourTooltip");
     let title = document.getElementById("UITourTooltipTitle");
     let desc = document.getElementById("UITourTooltipDescription");
     let icon = document.getElementById("UITourTooltipIcon");
     let buttons = document.getElementById("UITourTooltipButtons");
 
     // Disable the animation to prevent the mouse clicks from hitting the main
     // window during the transition instead of the buttons in the popup.
     popup.setAttribute("animate", "false");
 
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
+    yield showInfoPromise("urlbar", "a title", "some text", "image.png");
 
-      is(title.textContent, "a title", "Popup should have correct title");
-      is(desc.textContent, "some text", "Popup should have correct description text");
+    is(title.textContent, "a title", "Popup should have correct title");
+    is(desc.textContent, "some text", "Popup should have correct description text");
 
-      let imageURL = getRootDirectory(gTestPath) + "image.png";
-      imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
-      is(icon.src, imageURL,  "Popup should have correct icon shown");
-
-      is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+    let imageURL = getRootDirectory(gTestPath) + "image.png";
+    imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
+    is(icon.src, imageURL,  "Popup should have correct icon shown");
 
-      done();
-    });
+    is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+  }),
 
-    gContentAPI.showInfo("urlbar", "a title", "some text", "image.png");
-  },
-  function test_info_buttons_1(done) {
+  taskify(function* test_info_buttons_1() {
     let popup = document.getElementById("UITourTooltip");
     let title = document.getElementById("UITourTooltipTitle");
     let desc = document.getElementById("UITourTooltipDescription");
     let icon = document.getElementById("UITourTooltipIcon");
 
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
+    let buttons = gContentWindow.makeButtons();
 
-      is(title.textContent, "another title", "Popup should have correct title");
-      is(desc.textContent, "moar text", "Popup should have correct description text");
+    yield showInfoPromise("urlbar", "another title", "moar text", "./image.png", buttons);
+
+    is(title.textContent, "another title", "Popup should have correct title");
+    is(desc.textContent, "moar text", "Popup should have correct description text");
 
-      let imageURL = getRootDirectory(gTestPath) + "image.png";
-      imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
-      is(icon.src, imageURL,  "Popup should have correct icon shown");
+    let imageURL = getRootDirectory(gTestPath) + "image.png";
+    imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
+    is(icon.src, imageURL,  "Popup should have correct icon shown");
 
-      let buttons = document.getElementById("UITourTooltipButtons");
-      is(buttons.childElementCount, 2, "Popup should have two buttons");
-
-      is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
-      is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
-
-      is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
-      is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
+    buttons = document.getElementById("UITourTooltipButtons");
+    is(buttons.childElementCount, 2, "Popup should have two buttons");
 
-      popup.addEventListener("popuphidden", function onPopupHidden() {
-        popup.removeEventListener("popuphidden", onPopupHidden);
-        ok(true, "Popup should close automatically");
+    is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
+    is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
 
-        executeSoon(function() {
-          is(gContentWindow.callbackResult, "button1", "Correct callback should have been called");
+    is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
+    is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
 
-          done();
-        });
-      });
+    let promiseHidden = promisePanelElementHidden(window, popup);
+    EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window);
+    yield promiseHidden;
+
+    ok(true, "Popup should close automatically");
 
-      EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window);
-    });
+    yield waitForCallbackResultPromise();
 
-    let buttons = gContentWindow.makeButtons();
-    gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
-  },
-  function test_info_buttons_2(done) {
+    is(gContentWindow.callbackResult, "button1", "Correct callback should have been called");
+  }),
+  taskify(function* test_info_buttons_2() {
     let popup = document.getElementById("UITourTooltip");
     let title = document.getElementById("UITourTooltipTitle");
     let desc = document.getElementById("UITourTooltipDescription");
     let icon = document.getElementById("UITourTooltipIcon");
 
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
+    let buttons = gContentWindow.makeButtons();
 
-      is(title.textContent, "another title", "Popup should have correct title");
-      is(desc.textContent, "moar text", "Popup should have correct description text");
+    yield showInfoPromise("urlbar", "another title", "moar text", "./image.png", buttons);
+
+    is(title.textContent, "another title", "Popup should have correct title");
+    is(desc.textContent, "moar text", "Popup should have correct description text");
 
-      let imageURL = getRootDirectory(gTestPath) + "image.png";
-      imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
-      is(icon.src, imageURL,  "Popup should have correct icon shown");
+    let imageURL = getRootDirectory(gTestPath) + "image.png";
+    imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
+    is(icon.src, imageURL,  "Popup should have correct icon shown");
 
-      let buttons = document.getElementById("UITourTooltipButtons");
-      is(buttons.childElementCount, 2, "Popup should have two buttons");
-
-      is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
-      is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
-
-      is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
-      is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
+    buttons = document.getElementById("UITourTooltipButtons");
+    is(buttons.childElementCount, 2, "Popup should have two buttons");
 
-      popup.addEventListener("popuphidden", function onPopupHidden() {
-        popup.removeEventListener("popuphidden", onPopupHidden);
-        ok(true, "Popup should close automatically");
+    is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
+    is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
 
-        executeSoon(function() {
-          is(gContentWindow.callbackResult, "button2", "Correct callback should have been called");
+    is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
+    is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
 
-          done();
-        });
-      });
+    let promiseHidden = promisePanelElementHidden(window, popup);
+    EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
+    yield promiseHidden;
+
+    ok(true, "Popup should close automatically");
 
-      EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
-    });
+    yield waitForCallbackResultPromise();
 
-    let buttons = gContentWindow.makeButtons();
-    gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
-  },
+    is(gContentWindow.callbackResult, "button2", "Correct callback should have been called");
+  }),
 
-  function test_info_close_button(done) {
+  taskify(function* test_info_close_button() {
     let popup = document.getElementById("UITourTooltip");
     let closeButton = document.getElementById("UITourTooltipClose");
+    let infoOptions = gContentWindow.makeInfoOptions();
 
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
-      EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
-      executeSoon(function() {
-        is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
-        done();
-      });
-    });
+    yield showInfoPromise("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
+
+    EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
 
-    let infoOptions = gContentWindow.makeInfoOptions();
-    gContentAPI.showInfo("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
-  },
+    yield waitForCallbackResultPromise();
 
-  function test_info_target_callback(done) {
+    is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
+  }),
+
+  taskify(function* test_info_target_callback() {
     let popup = document.getElementById("UITourTooltip");
-    popup.addEventListener("popupshown", function onPopupShown() {
-      popup.removeEventListener("popupshown", onPopupShown);
-      PanelUI.show().then(() => {
-        is(gContentWindow.callbackResult, "target", "target callback called");
-        is(gContentWindow.callbackData.target, "appMenu", "target callback was from the appMenu");
-        is(gContentWindow.callbackData.type, "popupshown", "target callback was from the mousedown");
-        popup.removeAttribute("animate");
-        done();
-      });
-    });
+    let infoOptions = gContentWindow.makeInfoOptions();
+
+    yield showInfoPromise("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions);
+
+    yield PanelUI.show();
+
+    yield waitForCallbackResultPromise();
 
-    let infoOptions = gContentWindow.makeInfoOptions();
-    gContentAPI.showInfo("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions);
-  },
+    is(gContentWindow.callbackResult, "target", "target callback called");
+    is(gContentWindow.callbackData.target, "appMenu", "target callback was from the appMenu");
+    is(gContentWindow.callbackData.type, "popupshown", "target callback was from the mousedown");
+
+    // Cleanup.
+    yield hideInfoPromise();
+
+    popup.removeAttribute("animate");
+  }),
 ];
--- a/browser/modules/test/browser_UITour_detach_tab.js
+++ b/browser/modules/test/browser_UITour_detach_tab.js
@@ -6,78 +6,84 @@
  */
 
 "use strict";
 
 let gTestTab;
 let gContentAPI;
 let gContentWindow;
 let gContentDoc;
-let highlight = document.getElementById("UITourHighlight");
-let tooltip = document.getElementById("UITourTooltip");
 
 Components.utils.import("resource:///modules/UITour.jsm");
 
 function test() {
   registerCleanupFunction(function() {
     gContentDoc = null;
   });
   UITourTest();
 }
 
+/**
+ * When tab is changed we're tearing the tour down. So the UITour client has to always be aware of this
+ * fact and therefore listens to visibilitychange events.
+ * In particular this scenario happens for detaching the tab (ie. moving it to a new window).
+ */
 let tests = [
-  function test_move_tab_to_new_window(done) {
-    let gOpenedWindow;
+  taskify(function* test_move_tab_to_new_window(done) {
     let onVisibilityChange = (aEvent) => {
       if (!document.hidden && window != UITour.getChromeWindow(aEvent.target)) {
         gContentAPI.showHighlight("appMenu");
       }
     };
-    let onDOMWindowDestroyed = (aWindow, aTopic, aData) => {
-      if (gOpenedWindow && aWindow == gOpenedWindow) {
+
+    let highlight = document.getElementById("UITourHighlight");
+    let windowDestroyedDeferred = Promise.defer();
+    let onDOMWindowDestroyed = (aWindow) => {
+      if (gContentWindow && aWindow == gContentWindow) {
         Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
-        done();
-      }
-    };
-    let onBrowserDelayedStartup = (aWindow, aTopic, aData) => {
-      gOpenedWindow = aWindow;
-      Services.obs.removeObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished");
-      try {
-        let newWindowHighlight = gOpenedWindow.document.getElementById("UITourHighlight");
-        let selectedTab = aWindow.gBrowser.selectedTab;
-        is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
-        ok(UITour.originTabs && UITour.originTabs.has(aWindow), "Window should be known");
-        ok(UITour.originTabs.get(aWindow).has(selectedTab), "Tab should be known");
-        waitForElementToBeVisible(newWindowHighlight, function checkHighlightIsThere() {
-          let shownPromise = promisePanelShown(aWindow);
-          gContentAPI.showMenu("appMenu");
-          shownPromise.then(() => {
-            isnot(aWindow.PanelUI.panel.state, "closed", "Panel should be open");
-            ok(aWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
-            gContentAPI.hideHighlight();
-            gContentAPI.hideMenu("appMenu");
-            gTestTab = null;
-            aWindow.close();
-          }).then(null, Components.utils.reportError);
-        }, "Highlight should be shown in new window.");
-      } catch (ex) {
-        Cu.reportError(ex);
-        ok(false, "An error occurred running UITour tab detach test.");
-      } finally {
-        gContentDoc.removeEventListener("visibilitychange", onVisibilityChange, false);
-        Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
+        windowDestroyedDeferred.resolve();
       }
     };
 
-    Services.obs.addObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished", false);
+    let browserStartupDeferred = Promise.defer();
+    Services.obs.addObserver(function onBrowserDelayedStartup(aWindow) {
+      Services.obs.removeObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished");
+      browserStartupDeferred.resolve(aWindow);
+    }, "browser-delayed-startup-finished", false);
+
     // NB: we're using this rather than gContentWindow.document because the latter wouldn't
     // have an XRayWrapper, and we need to compare this to the doc we get using this method
     // later on...
     gContentDoc = gBrowser.selectedTab.linkedBrowser.contentDocument;
     gContentDoc.addEventListener("visibilitychange", onVisibilityChange, false);
     gContentAPI.showHighlight("appMenu");
-    waitForElementToBeVisible(highlight, function checkForInitialHighlight() {
-      gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-    });
+
+    yield elementVisiblePromise(highlight);
+
+    gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+
+    gContentWindow = yield browserStartupDeferred.promise;
+
+    // This highlight should be shown thanks to the visibilitychange listener.
+    let newWindowHighlight = gContentWindow.document.getElementById("UITourHighlight");
+    yield elementVisiblePromise(newWindowHighlight);
+
+    let selectedTab = gContentWindow.gBrowser.selectedTab;
+    is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
+    ok(UITour.originTabs && UITour.originTabs.has(gContentWindow), "Window should be known");
+    ok(UITour.originTabs.get(gContentWindow).has(selectedTab), "Tab should be known");
 
-  },
+    let shownPromise = promisePanelShown(gContentWindow);
+    gContentAPI.showMenu("appMenu");
+    yield shownPromise;
+
+    isnot(gContentWindow.PanelUI.panel.state, "closed", "Panel should be open");
+    ok(gContentWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
+    gContentAPI.hideHighlight();
+    gContentAPI.hideMenu("appMenu");
+    gTestTab = null;
+
+    Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
+    gContentWindow.close();
+
+    yield windowDestroyedDeferred.promise;
+  }),
 ];
-
--- a/browser/modules/test/browser_UITour_registerPageID.js
+++ b/browser/modules/test/browser_UITour_registerPageID.js
@@ -61,50 +61,52 @@ let tests = [
     Services.prefs.setCharPref("browser.uitour.seenPageIDs",
                                data);
 
     resetSeenPageIDsLazyGetter();
     checkExpectedSeenPageIDs(["savedID1", "savedID2"]);
 
     done();
   },
-  function test_seenPageIDs_set_1(done) {
+  taskify(function* test_seenPageIDs_set_1() {
     gContentAPI.registerPageID("testpage1");
 
+    yield waitForConditionPromise(() => UITour.seenPageIDs.size == 3, "Waiting for page to be registered.");
+
     checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1"]);
 
     const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
     const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
 
     let bucket = PREFIX + "UITour" + SEP + "testpage1";
     is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
 
     gBrowser.selectedTab = gBrowser.addTab("about:blank");
     bucket = PREFIX + "UITour" + SEP + "testpage1" + SEP + "inactive" + SEP + "1m";
     is(BrowserUITelemetry.currentBucket, bucket,
        "After switching tabs, bucket should be expiring");
 
     gBrowser.removeTab(gBrowser.selectedTab);
     gBrowser.selectedTab = gTestTab;
     BrowserUITelemetry.setBucket(null);
-    done();
-  },
-  function test_seenPageIDs_set_2(done) {
+  }),
+  taskify(function* test_seenPageIDs_set_2() {
     gContentAPI.registerPageID("testpage2");
 
+    yield waitForConditionPromise(() => UITour.seenPageIDs.size == 4, "Waiting for page to be registered.");
+
     checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1", "testpage2"]);
 
     const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
     const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
 
     let bucket = PREFIX + "UITour" + SEP + "testpage2";
     is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
 
     gBrowser.removeTab(gTestTab);
     gTestTab = null;
     bucket = PREFIX + "UITour" + SEP + "testpage2" + SEP + "closed" + SEP + "1m";
     is(BrowserUITelemetry.currentBucket, bucket,
        "After closing tab, bucket should be expiring");
 
     BrowserUITelemetry.setBucket(null);
-    done();
-  },
+  }),
 ];
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -1,33 +1,57 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource:///modules/UITour.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
-function waitForCondition(condition, nextTest, errorMsg) {
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+function waitForConditionPromise(condition, timeoutMsg) {
+  let defer = Promise.defer();
+  let tries = 0;
+  function checkCondition() {
+    if (tries >= NUMBER_OF_TRIES) {
+      defer.reject(timeoutMsg);
     }
     var conditionPassed;
     try {
       conditionPassed = condition();
     } catch (e) {
-      ok(false, e + "\n" + e.stack);
-      conditionPassed = false;
+      return defer.reject(e);
     }
     if (conditionPassed) {
-      moveOn();
+      return defer.resolve();
     }
     tries++;
-  }, 100);
-  var moveOn = function() { clearInterval(interval); nextTest(); };
+    setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+  }
+  setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+  return defer.promise;
+}
+
+function waitForCondition(condition, nextTest, errorMsg) {
+  waitForConditionPromise(condition, errorMsg).then(nextTest, (reason) => {
+    ok(false, reason + (reason.stack ? "\n" + e.stack : ""));
+  });
+}
+
+/**
+ * Wrapper to partially transition tests to Task.
+ */
+function taskify(fun) {
+  return (done) => {
+    return Task.spawn(fun).then(done, (reason) => {
+      ok(false, reason);
+      done();
+    });
+  }
 }
 
 function is_hidden(element) {
   var style = element.ownerDocument.defaultView.getComputedStyle(element, "");
   if (style.display == "none")
     return true;
   if (style.visibility != "visible")
     return true;
@@ -75,44 +99,97 @@ function waitForElementToBeHidden(elemen
   waitForCondition(() => is_hidden(element),
                    () => {
                      ok(true, msg);
                      nextTest();
                    },
                    "Timeout waiting for invisibility: " + msg);
 }
 
+function elementVisiblePromise(element, msg) {
+  return waitForConditionPromise(() => is_visible(element), "Timeout waiting for visibility: " + msg);
+}
+
+function elementHiddenPromise(element, msg) {
+  return waitForConditionPromise(() => is_hidden(element), "Timeout waiting for invisibility: " + msg);
+}
+
 function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) {
   waitForCondition(() => is_visible(popup) && popup.popupBoxObject.anchorNode == anchorNode,
                    () => {
                      ok(true, msg);
                      is_element_visible(popup, "Popup should be visible");
                      nextTest();
                    },
                    "Timeout waiting for popup at anchor: " + msg);
 }
 
+function hideInfoPromise(...args) {
+  let popup = document.getElementById("UITourTooltip");
+  gContentAPI.hideInfo.apply(gContentAPI, args);
+  return promisePanelElementHidden(window, popup);
+}
+
+function showInfoPromise(...args) {
+  let popup = document.getElementById("UITourTooltip");
+  gContentAPI.showInfo.apply(gContentAPI, args);
+  return promisePanelElementShown(window, popup);
+}
+
+function waitForCallbackResultPromise() {
+  return waitForConditionPromise(() => {
+    return gContentWindow.callbackResult;
+  }, "callback should be called");
+}
+
+function addPinnedTabPromise() {
+  gContentAPI.addPinnedTab();
+  return waitForConditionPromise(() => {
+    let tabInfo = UITour.pinnedTabs.get(window);
+    if (!tabInfo) {
+      return false;
+    }
+    return tabInfo.tab.pinned;
+  });
+}
+
+function removePinnedTabPromise() {
+  gContentAPI.removePinnedTab();
+  return waitForConditionPromise(() => {
+    let tabInfo = UITour.pinnedTabs.get(window);
+    return tabInfo == null;
+  });
+}
+
 function promisePanelShown(win) {
   let panelEl = win.PanelUI.panel;
   return promisePanelElementShown(win, panelEl);
 }
 
-function promisePanelElementShown(win, aPanel) {
+function promisePanelElementEvent(win, aPanel, aEvent) {
   let deferred = Promise.defer();
   let timeoutId = win.setTimeout(() => {
     deferred.reject("Panel did not show within 5 seconds.");
   }, 5000);
-  aPanel.addEventListener("popupshown", function onPanelOpen(e) {
-    aPanel.removeEventListener("popupshown", onPanelOpen);
+  aPanel.addEventListener(aEvent, function onPanelEvent(e) {
+    aPanel.removeEventListener(aEvent, onPanelEvent);
     win.clearTimeout(timeoutId);
     deferred.resolve();
   });
   return deferred.promise;
 }
 
+function promisePanelElementShown(win, aPanel) {
+  return promisePanelElementEvent(win, aPanel, "popupshown");
+}
+
+function promisePanelElementHidden(win, aPanel) {
+  return promisePanelElementEvent(win, aPanel, "popuphidden");
+}
+
 function is_element_hidden(element, msg) {
   isnot(element, null, "Element should not be null, when checking visibility");
   ok(is_hidden(element), msg);
 }
 
 function loadUITourTestPage(callback, host = "https://example.com/") {
   if (gTestTab)
     gBrowser.removeTab(gTestTab);