diff options
Diffstat (limited to 'browser/components/uitour/test/head.js')
-rw-r--r-- | browser/components/uitour/test/head.js | 449 |
1 files changed, 449 insertions, 0 deletions
diff --git a/browser/components/uitour/test/head.js b/browser/components/uitour/test/head.js new file mode 100644 index 000000000..2b5b994ae --- /dev/null +++ b/browser/components/uitour/test/head.js @@ -0,0 +1,449 @@ +"use strict"; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UITour", + "resource:///modules/UITour.jsm"); + + +const SINGLE_TRY_TIMEOUT = 100; +const NUMBER_OF_TRIES = 30; + +function waitForConditionPromise(condition, timeoutMsg, tryCount=NUMBER_OF_TRIES) { + let defer = Promise.defer(); + let tries = 0; + function checkCondition() { + if (tries >= tryCount) { + defer.reject(timeoutMsg); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + return defer.reject(e); + } + if (conditionPassed) { + return defer.resolve(); + } + tries++; + setTimeout(checkCondition, SINGLE_TRY_TIMEOUT); + return undefined; + } + 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" + reason.stack : "")); + }); +} + +/** + * Wrapper to partially transition tests to Task. Use `add_UITour_task` instead for new tests. + */ +function taskify(fun) { + return (done) => { + // Output the inner function name otherwise no name will be output. + info("\t" + fun.name); + return Task.spawn(fun).then(done, (reason) => { + ok(false, reason); + done(); + }); + }; +} + +function is_hidden(element) { + var style = element.ownerGlobal.getComputedStyle(element); + if (style.display == "none") + return true; + if (style.visibility != "visible") + return true; + if (style.display == "-moz-popup") + return ["hiding", "closed"].indexOf(element.state) != -1; + + // Hiding a parent element will hide all its children + if (element.parentNode != element.ownerDocument) + return is_hidden(element.parentNode); + + return false; +} + +function is_visible(element) { + var style = element.ownerGlobal.getComputedStyle(element); + if (style.display == "none") + return false; + if (style.visibility != "visible") + return false; + if (style.display == "-moz-popup" && element.state != "open") + return false; + + // Hiding a parent element will hide all its children + if (element.parentNode != element.ownerDocument) + return is_visible(element.parentNode); + + return true; +} + +function is_element_visible(element, msg) { + isnot(element, null, "Element should not be null, when checking visibility"); + ok(is_visible(element), msg); +} + +function waitForElementToBeVisible(element, nextTest, msg) { + waitForCondition(() => is_visible(element), + () => { + ok(true, msg); + nextTest(); + }, + "Timeout waiting for visibility: " + msg); +} + +function waitForElementToBeHidden(element, nextTest, msg) { + 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 getConfigurationPromise(configName) { + return ContentTask.spawn(gTestTab.linkedBrowser, configName, configName => { + return new Promise((resolve) => { + let contentWin = Components.utils.waiveXrays(content); + contentWin.Mozilla.UITour.getConfiguration(configName, resolve); + }); + }); +} + +function hideInfoPromise(...args) { + let popup = document.getElementById("UITourTooltip"); + gContentAPI.hideInfo.apply(gContentAPI, args); + return promisePanelElementHidden(window, popup); +} + +/** + * `buttons` and `options` require functions from the content scope so we take a + * function name to call to generate the buttons/options instead of the + * buttons/options themselves. This makes the signature differ from the content one. + */ +function showInfoPromise(target, title, text, icon, buttonsFunctionName, optionsFunctionName) { + let popup = document.getElementById("UITourTooltip"); + let shownPromise = promisePanelElementShown(window, popup); + return ContentTask.spawn(gTestTab.linkedBrowser, [...arguments], args => { + let contentWin = Components.utils.waiveXrays(content); + let [target, title, text, icon, buttonsFunctionName, optionsFunctionName] = args; + let buttons = buttonsFunctionName ? contentWin[buttonsFunctionName]() : null; + let options = optionsFunctionName ? contentWin[optionsFunctionName]() : null; + contentWin.Mozilla.UITour.showInfo(target, title, text, icon, buttons, options); + }).then(() => shownPromise); +} + +function showHighlightPromise(...args) { + let popup = document.getElementById("UITourHighlightContainer"); + gContentAPI.showHighlight.apply(gContentAPI, args); + return promisePanelElementShown(window, popup); +} + +function showMenuPromise(name) { + return ContentTask.spawn(gTestTab.linkedBrowser, name, name => { + return new Promise((resolve) => { + let contentWin = Components.utils.waiveXrays(content); + contentWin.Mozilla.UITour.showMenu(name, resolve); + }); + }); +} + +function waitForCallbackResultPromise() { + return ContentTask.spawn(gTestTab.linkedBrowser, null, function*() { + let contentWin = Components.utils.waiveXrays(content); + yield ContentTaskUtils.waitForCondition(() => { + return contentWin.callbackResult; + }, "callback should be called"); + return { + data: contentWin.callbackData, + result: contentWin.callbackResult, + }; + }); +} + +function promisePanelShown(win) { + let panelEl = win.PanelUI.panel; + return promisePanelElementShown(win, panelEl); +} + +function promisePanelElementEvent(win, aPanel, aEvent) { + return new Promise((resolve, reject) => { + let timeoutId = win.setTimeout(() => { + aPanel.removeEventListener(aEvent, onPanelEvent); + reject(aEvent + " event did not happen within 5 seconds."); + }, 5000); + + function onPanelEvent(e) { + aPanel.removeEventListener(aEvent, onPanelEvent); + win.clearTimeout(timeoutId); + // Wait one tick to let UITour.jsm process the event as well. + executeSoon(resolve); + } + + aPanel.addEventListener(aEvent, onPanelEvent); + }); +} + +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 isTourBrowser(aBrowser) { + let chromeWindow = aBrowser.ownerGlobal; + return UITour.tourBrowsersByWindow.has(chromeWindow) && + UITour.tourBrowsersByWindow.get(chromeWindow).has(aBrowser); +} + +function promisePageEvent() { + return new Promise((resolve) => { + Services.mm.addMessageListener("UITour:onPageEvent", function onPageEvent(aMessage) { + Services.mm.removeMessageListener("UITour:onPageEvent", onPageEvent); + SimpleTest.executeSoon(resolve); + }); + }); +} + +function loadUITourTestPage(callback, host = "https://example.org/") { + if (gTestTab) + gBrowser.removeTab(gTestTab); + + let url = getRootDirectory(gTestPath) + "uitour.html"; + url = url.replace("chrome://mochitests/content/", host); + + gTestTab = gBrowser.addTab(url); + gBrowser.selectedTab = gTestTab; + + gTestTab.linkedBrowser.addEventListener("load", function onLoad() { + gTestTab.linkedBrowser.removeEventListener("load", onLoad, true); + + if (gMultiProcessBrowser) { + // When e10s is enabled, make gContentAPI and gContentWindow proxies which has every property + // return a function which calls the method of the same name on + // contentWin.Mozilla.UITour/contentWin in a ContentTask. + let contentWinHandler = { + get(target, prop, receiver) { + return (...args) => { + let taskArgs = { + methodName: prop, + args, + }; + return ContentTask.spawn(gTestTab.linkedBrowser, taskArgs, args => { + let contentWin = Components.utils.waiveXrays(content); + return contentWin[args.methodName].apply(contentWin, args.args); + }); + }; + }, + }; + gContentWindow = new Proxy({}, contentWinHandler); + + let UITourHandler = { + get(target, prop, receiver) { + return (...args) => { + let browser = gTestTab.linkedBrowser; + const proxyFunctionName = "UITourHandler:proxiedfunction-"; + // We need to proxy any callback functions using messages: + let callbackMap = new Map(); + let fnIndices = []; + args = args.map((arg, index) => { + // Replace function arguments with "", and add them to the list of + // forwarded functions. We'll construct a function on the content-side + // that forwards all its arguments to a message, and we'll listen for + // those messages on our side and call the corresponding function with + // the arguments we got from the content side. + if (typeof arg == "function") { + callbackMap.set(index, arg); + fnIndices.push(index); + let handler = function(msg) { + // Please note that this handler assumes that the callback is used only once. + // That means that a single gContentAPI.observer() call can't be used to observe + // multiple events. + browser.messageManager.removeMessageListener(proxyFunctionName + index, handler); + callbackMap.get(index).apply(null, msg.data); + }; + browser.messageManager.addMessageListener(proxyFunctionName + index, handler); + return ""; + } + return arg; + }); + let taskArgs = { + methodName: prop, + args, + fnIndices, + }; + return ContentTask.spawn(browser, taskArgs, function*(args) { + let contentWin = Components.utils.waiveXrays(content); + let callbacksCalled = 0; + let resolveCallbackPromise; + let allCallbacksCalledPromise = new Promise(resolve => resolveCallbackPromise = resolve); + let argumentsWithFunctions = args.args.map((arg, index) => { + if (arg === "" && args.fnIndices.includes(index)) { + return function() { + callbacksCalled++; + sendAsyncMessage("UITourHandler:proxiedfunction-" + index, Array.from(arguments)); + if (callbacksCalled >= args.fnIndices.length) { + resolveCallbackPromise(); + } + }; + } + return arg; + }); + let rv = contentWin.Mozilla.UITour[args.methodName].apply(contentWin.Mozilla.UITour, + argumentsWithFunctions); + if (args.fnIndices.length) { + yield allCallbacksCalledPromise; + } + return rv; + }); + }; + }, + }; + gContentAPI = new Proxy({}, UITourHandler); + } else { + gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView); + gContentAPI = gContentWindow.Mozilla.UITour; + } + + waitForFocus(callback, gTestTab.linkedBrowser); + }, true); +} + +// Wrapper for UITourTest to be used by add_task tests. +function* setup_UITourTest() { + return UITourTest(true); +} + +// Use `add_task(setup_UITourTest);` instead as we will fold this into `setup_UITourTest` once all tests are using `add_UITour_task`. +function UITourTest(usingAddTask = false) { + Services.prefs.setBoolPref("browser.uitour.enabled", true); + let testHttpsUri = Services.io.newURI("https://example.org", null, null); + let testHttpUri = Services.io.newURI("http://example.org", null, null); + Services.perms.add(testHttpsUri, "uitour", Services.perms.ALLOW_ACTION); + Services.perms.add(testHttpUri, "uitour", Services.perms.ALLOW_ACTION); + + // If a test file is using add_task, we don't need to have a test function or + // call `waitForExplicitFinish`. + if (!usingAddTask) { + waitForExplicitFinish(); + } + + registerCleanupFunction(function() { + delete window.gContentWindow; + delete window.gContentAPI; + if (gTestTab) + gBrowser.removeTab(gTestTab); + delete window.gTestTab; + Services.prefs.clearUserPref("browser.uitour.enabled"); + Services.perms.remove(testHttpsUri, "uitour"); + Services.perms.remove(testHttpUri, "uitour"); + }); + + // When using tasks, the harness will call the next added task for us. + if (!usingAddTask) { + nextTest(); + } +} + +function done(usingAddTask = false) { + info("== Done test, doing shared checks before teardown =="); + return new Promise((resolve) => { + executeSoon(() => { + if (gTestTab) + gBrowser.removeTab(gTestTab); + gTestTab = null; + + let highlight = document.getElementById("UITourHighlightContainer"); + is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed"); + + let tooltip = document.getElementById("UITourTooltip"); + is_element_hidden(tooltip, "Tooltip should be closed/hidden after UITour tab is closed"); + + ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up"); + ok(!PanelUI.panel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen"); + isnot(PanelUI.panel.state, "open", "The panel shouldn't be open"); + is(document.getElementById("PanelUI-menu-button").hasAttribute("open"), false, "Menu button should know that the menu is closed"); + + info("Done shared checks"); + if (usingAddTask) { + executeSoon(resolve); + } else { + executeSoon(nextTest); + } + }); + }); +} + +function nextTest() { + if (tests.length == 0) { + info("finished tests in this file"); + finish(); + return; + } + let test = tests.shift(); + info("Starting " + test.name); + waitForFocus(function() { + loadUITourTestPage(function() { + test(done); + }); + }); +} + +/** + * All new tests that need the help of `loadUITourTestPage` should use this + * wrapper around their test's generator function to reduce boilerplate. + */ +function add_UITour_task(func) { + let genFun = function*() { + yield new Promise((resolve) => { + waitForFocus(function() { + loadUITourTestPage(function() { + let funcPromise = Task.spawn(func) + .then(() => done(true), + (reason) => { + ok(false, reason); + return done(true); + }); + resolve(funcPromise); + }); + }); + }); + }; + Object.defineProperty(genFun, "name", { + configurable: true, + value: func.name, + }); + add_task(genFun); +} |