From 1b9c50100ddd3547713f85816c0d5808f9b4188d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Fri, 24 Oct 2014 17:19:00 +0200 Subject: [PATCH] Bug 1073238 - Split UITour.jsm into chrome and content parts that communicate via messages. r=MattN --- browser/base/content/browser.js | 1 + browser/base/content/content-UITour.js | 86 +++++++ browser/base/content/content.js | 11 - browser/base/jar.mn | 1 + browser/components/nsBrowserGlue.js | 12 + browser/modules/UITour.jsm | 152 +++++-------- browser/modules/test/browser_UITour.js | 45 ++-- browser/modules/test/browser_UITour2.js | 40 ++-- browser/modules/test/browser_UITour3.js | 209 ++++++++---------- .../modules/test/browser_UITour_detach_tab.js | 90 ++++---- .../test/browser_UITour_registerPageID.js | 14 +- browser/modules/test/head.js | 105 +++++++-- 12 files changed, 440 insertions(+), 326 deletions(-) create mode 100644 browser/base/content/content-UITour.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index e86b41e9da8d..c4de7d7fe120 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -853,6 +853,7 @@ var gBrowserInit = { 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 diff --git a/browser/base/content/content-UITour.js b/browser/base/content/content-UITour.js new file mode 100644 index 000000000000..5d2df9d1c04d --- /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); diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 30351d937e2b..eda387e8248c 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -25,8 +25,6 @@ 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"); @@ -122,15 +120,6 @@ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { .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 = { diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 0025b9d4c89e..fc20a1fc718a 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -77,6 +77,7 @@ browser.jar: * 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) diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 71650941813d..b8b6f6e198c2 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -17,6 +17,9 @@ 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"); @@ -2553,3 +2556,12 @@ let E10SUINotification = { 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); +}); diff --git a/browser/modules/UITour.jsm b/browser/modules/UITour.jsm index c30a840a990e..0680551d97e2 100644 --- a/browser/modules/UITour.jsm +++ b/browser/modules/UITour.jsm @@ -25,8 +25,6 @@ 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; @@ -222,18 +220,13 @@ this.UITour = { 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; @@ -246,20 +239,22 @@ this.UITour = { 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) { @@ -315,7 +310,7 @@ this.UITour = { 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) { @@ -329,7 +324,7 @@ this.UITour = { }; 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; @@ -349,7 +344,7 @@ this.UITour = { 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; } @@ -382,7 +377,7 @@ this.UITour = { case "showMenu": { this.showMenu(window, data.name, () => { if (typeof data.showCallbackID == "string") - this.sendPageCallback(contentDocument, data.showCallbackID); + this.sendPageCallback(messageManager, data.showCallbackID); }); break; } @@ -428,7 +423,7 @@ this.UITour = { return false; } - this.getConfiguration(contentDocument, data.configuration, data.callbackID); + this.getConfiguration(messageManager, window, data.configuration, data.callbackID); break; } @@ -450,19 +445,22 @@ this.UITour = { // 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; }, @@ -621,44 +619,7 @@ this.UITour = { .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")) @@ -670,9 +631,9 @@ this.UITour = { 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; @@ -683,16 +644,9 @@ this.UITour = { 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) { @@ -966,7 +920,7 @@ this.UITour = { /** * Show an info panel. * - * @param {Document} aContentDocument + * @param {nsIMessageSender} aMessageManager * @param {Node} aAnchor * @param {String} [aTitle=""] * @param {String} [aDescription=""] @@ -975,7 +929,7 @@ this.UITour = { * @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(); @@ -1014,7 +968,7 @@ this.UITour = { let callbackID = button.callbackID; el.addEventListener("command", event => { tooltip.hidePopup(); - this.sendPageCallback(aContentDocument, callbackID); + this.sendPageCallback(aMessageManager, callbackID); }); tooltipButtons.appendChild(el); @@ -1026,7 +980,7 @@ this.UITour = { 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); @@ -1035,7 +989,7 @@ this.UITour = { 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); @@ -1214,13 +1168,13 @@ this.UITour = { 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; @@ -1228,7 +1182,7 @@ this.UITour = { 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); @@ -1236,12 +1190,12 @@ this.UITour = { } }, - 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; } @@ -1268,16 +1222,16 @@ this.UITour = { 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; @@ -1292,7 +1246,7 @@ this.UITour = { } CustomizableUI.addWidgetToArea(aTarget.widgetName, CustomizableUI.AREA_NAVBAR); - this.sendPageCallback(aContentDocument, aCallbackID); + this.sendPageCallback(aMessageManager, aCallbackID); }, _addAnnotationPanelMutationObserver: function(aPanelEl) { diff --git a/browser/modules/test/browser_UITour.js b/browser/modules/test/browser_UITour.js index 3309f922738f..067163dd9466 100644 --- a/browser/modules/test/browser_UITour.js +++ b/browser/modules/test/browser_UITour.js @@ -81,12 +81,16 @@ let tests = [ 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()"); @@ -302,33 +306,27 @@ let tests = [ 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"]; @@ -368,8 +366,9 @@ let tests = [ }, // 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; + }), ]; diff --git a/browser/modules/test/browser_UITour2.js b/browser/modules/test/browser_UITour2.js index 40efaaaae1ff..cba2d6aee257 100644 --- a/browser/modules/test/browser_UITour2.js +++ b/browser/modules/test/browser_UITour2.js @@ -56,19 +56,22 @@ let tests = [ 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()"); @@ -76,28 +79,29 @@ let tests = [ 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"); - - done(); - }, - function test_menu(done) { + }), + taskify(function* test_menu() { let bookmarksMenuButton = document.getElementById("bookmarks-menu-button"); - ise(bookmarksMenuButton.open, false, "Menu should initially be closed"); + ise(bookmarksMenuButton.open, false, "Menu should initially be closed"); 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()"); + }), ]; diff --git a/browser/modules/test/browser_UITour3.js b/browser/modules/test/browser_UITour3.js index 5273967175df..6c66703ac3c8 100644 --- a/browser/modules/test/browser_UITour3.js +++ b/browser/modules/test/browser_UITour3.js @@ -16,7 +16,7 @@ function test() { } 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"); @@ -27,139 +27,122 @@ let tests = [ // 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"); + 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"); + is(buttons.hasChildNodes(), false, "Popup should have no buttons"); + }), - done(); - }); - - 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); - - 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 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"); - - popup.addEventListener("popuphidden", function onPopupHidden() { - popup.removeEventListener("popuphidden", onPopupHidden); - ok(true, "Popup should close automatically"); - - executeSoon(function() { - is(gContentWindow.callbackResult, "button1", "Correct callback should have been called"); - - done(); - }); - }); - - EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window); - }); - let buttons = gContentWindow.makeButtons(); - gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons); - }, - function test_info_buttons_2(done) { + + 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"); + + 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"); + + let promiseHidden = promisePanelElementHidden(window, popup); + EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window); + yield promiseHidden; + + ok(true, "Popup should close automatically"); + + yield waitForCallbackResultPromise(); + + 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); - - 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 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"); - - popup.addEventListener("popuphidden", function onPopupHidden() { - popup.removeEventListener("popuphidden", onPopupHidden); - ok(true, "Popup should close automatically"); - - executeSoon(function() { - is(gContentWindow.callbackResult, "button2", "Correct callback should have been called"); - - done(); - }); - }); - - EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window); - }); - let buttons = gContentWindow.makeButtons(); - gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons); - }, - function test_info_close_button(done) { + 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"); + + 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"); + + let promiseHidden = promisePanelElementHidden(window, popup); + EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window); + yield promiseHidden; + + ok(true, "Popup should close automatically"); + + yield waitForCallbackResultPromise(); + + is(gContentWindow.callbackResult, "button2", "Correct callback should have been called"); + }), + + taskify(function* test_info_close_button() { let popup = document.getElementById("UITourTooltip"); let closeButton = document.getElementById("UITourTooltipClose"); - - popup.addEventListener("popupshown", function onPopupShown() { - popup.removeEventListener("popupshown", onPopupShown); - EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); - executeSoon(function() { - is(gContentWindow.callbackResult, "closeButton", "Close button callback called"); - done(); - }); - }); - let infoOptions = gContentWindow.makeInfoOptions(); - gContentAPI.showInfo("urlbar", "Close me", "X marks the spot", null, null, infoOptions); - }, - function test_info_target_callback(done) { + yield showInfoPromise("urlbar", "Close me", "X marks the spot", null, null, infoOptions); + + EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); + + yield waitForCallbackResultPromise(); + + 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(); - gContentAPI.showInfo("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions); - }, + + yield showInfoPromise("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions); + + yield PanelUI.show(); + + yield waitForCallbackResultPromise(); + + 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"); + }), ]; diff --git a/browser/modules/test/browser_UITour_detach_tab.js b/browser/modules/test/browser_UITour_detach_tab.js index d979d842506c..047afd915323 100644 --- a/browser/modules/test/browser_UITour_detach_tab.js +++ b/browser/modules/test/browser_UITour_detach_tab.js @@ -11,8 +11,6 @@ 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"); @@ -23,61 +21,69 @@ function test() { 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; + }), ]; - diff --git a/browser/modules/test/browser_UITour_registerPageID.js b/browser/modules/test/browser_UITour_registerPageID.js index 08e0f3a069ea..10d077ad148f 100644 --- a/browser/modules/test/browser_UITour_registerPageID.js +++ b/browser/modules/test/browser_UITour_registerPageID.js @@ -66,9 +66,11 @@ let tests = [ 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; @@ -85,11 +87,12 @@ let tests = [ 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; @@ -105,6 +108,5 @@ let tests = [ "After closing tab, bucket should be expiring"); BrowserUITelemetry.setBucket(null); - done(); - }, + }), ]; diff --git a/browser/modules/test/head.js b/browser/modules/test/head.js index 99f58f3707ae..ea8134fc8770 100644 --- a/browser/modules/test/head.js +++ b/browser/modules/test/head.js @@ -2,27 +2,51 @@ * 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) { @@ -80,6 +104,14 @@ function waitForElementToBeHidden(element, nextTest, msg) { "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, () => { @@ -90,24 +122,69 @@ function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) { "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);