summaryrefslogtreecommitdiffstats
path: root/browser/components/uitour/test/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/uitour/test/head.js')
-rw-r--r--browser/components/uitour/test/head.js449
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);
+}