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.js539
1 files changed, 539 insertions, 0 deletions
diff --git a/browser/components/uitour/test/head.js b/browser/components/uitour/test/head.js
new file mode 100644
index 0000000000..07b941ba1c
--- /dev/null
+++ b/browser/components/uitour/test/head.js
@@ -0,0 +1,539 @@
+"use strict";
+
+// This file expects these globals to be defined by the test case.
+/* global gTestTab:true, gContentAPI:true, tests:false */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UITour: "resource:///modules/UITour.sys.mjs",
+});
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+let gProxyCallbackMap = new Map();
+
+function waitForConditionPromise(
+ condition,
+ timeoutMsg,
+ tryCount = NUMBER_OF_TRIES
+) {
+ return new Promise((resolve, reject) => {
+ let tries = 0;
+ function checkCondition() {
+ if (tries >= tryCount) {
+ reject(timeoutMsg);
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ return reject(e);
+ }
+ if (conditionPassed) {
+ return resolve();
+ }
+ tries++;
+ setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+ return undefined;
+ }
+ setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+ });
+}
+
+function waitForCondition(condition, nextTestFn, errorMsg) {
+ waitForConditionPromise(condition, errorMsg).then(nextTestFn, 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 doneFn => {
+ // Output the inner function name otherwise no name will be output.
+ info("\t" + fun.name);
+ return fun().then(doneFn, reason => {
+ console.error(reason);
+ ok(false, reason);
+ doneFn();
+ });
+ };
+}
+
+function is_hidden(element) {
+ let win = element.ownerGlobal;
+ let style = win.getComputedStyle(element);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+ if (win.XULPopupElement.isInstance(element)) {
+ return ["hiding", "closed"].includes(element.state);
+ }
+
+ // 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) {
+ let win = element.ownerGlobal;
+ let style = win.getComputedStyle(element);
+ if (style.display == "none") {
+ return false;
+ }
+ if (style.visibility != "visible") {
+ return false;
+ }
+ if (win.XULPopupElement.isInstance(element) && 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, nextTestFn, msg) {
+ waitForCondition(
+ () => is_visible(element),
+ () => {
+ ok(true, msg);
+ nextTestFn();
+ },
+ "Timeout waiting for visibility: " + msg
+ );
+}
+
+function waitForElementToBeHidden(element, nextTestFn, msg) {
+ waitForCondition(
+ () => is_hidden(element),
+ () => {
+ ok(true, msg);
+ nextTestFn();
+ },
+ "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, nextTestFn, msg) {
+ waitForCondition(
+ () => is_visible(popup) && popup.anchorNode == anchorNode,
+ () => {
+ ok(true, msg);
+ is_element_visible(popup, "Popup should be visible");
+ nextTestFn();
+ },
+ "Timeout waiting for popup at anchor: " + msg
+ );
+}
+
+function getConfigurationPromise(configName) {
+ return SpecialPowers.spawn(
+ gTestTab.linkedBrowser,
+ [configName],
+ contentConfigName => {
+ return new Promise(resolve => {
+ let contentWin = Cu.waiveXrays(content);
+ contentWin.Mozilla.UITour.getConfiguration(contentConfigName, resolve);
+ });
+ }
+ );
+}
+
+function getShowHighlightTargetName() {
+ let highlight = document.getElementById("UITourHighlight");
+ return highlight.parentElement.getAttribute("targetName");
+}
+
+function getShowInfoTargetName() {
+ let tooltip = document.getElementById("UITourTooltip");
+ return tooltip.getAttribute("targetName");
+}
+
+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 SpecialPowers.spawn(gTestTab.linkedBrowser, [[...arguments]], args => {
+ let contentWin = Cu.waiveXrays(content);
+ let [
+ contentTarget,
+ contentTitle,
+ contentText,
+ contentIcon,
+ contentButtonsFunctionName,
+ contentOptionsFunctionName,
+ ] = args;
+ let buttons = contentButtonsFunctionName
+ ? contentWin[contentButtonsFunctionName]()
+ : null;
+ let options = contentOptionsFunctionName
+ ? contentWin[contentOptionsFunctionName]()
+ : null;
+ contentWin.Mozilla.UITour.showInfo(
+ contentTarget,
+ contentTitle,
+ contentText,
+ contentIcon,
+ 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 SpecialPowers.spawn(gTestTab.linkedBrowser, [name], contentName => {
+ return new Promise(resolve => {
+ let contentWin = Cu.waiveXrays(content);
+ contentWin.Mozilla.UITour.showMenu(contentName, resolve);
+ });
+ });
+}
+
+function waitForCallbackResultPromise() {
+ return SpecialPowers.spawn(gTestTab.linkedBrowser, [], async function () {
+ let contentWin = Cu.waiveXrays(content);
+ await 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.sys.mjs 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)
+ );
+}
+
+async function loadUITourTestPage(callback, host = "https://example.org/") {
+ if (gTestTab) {
+ gProxyCallbackMap.clear();
+ gBrowser.removeTab(gTestTab);
+ }
+
+ if (!window.gProxyCallbackMap) {
+ window.gProxyCallbackMap = gProxyCallbackMap;
+ }
+
+ let url = getRootDirectory(gTestPath) + "uitour.html";
+ url = url.replace("chrome://mochitests/content/", host);
+
+ gTestTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ // When e10s is enabled, make gContentAPI a proxy which has every property
+ // return a function which calls the method of the same name on
+ // contentWin.Mozilla.UITour in a ContentTask.
+ let UITourHandler = {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let browser = gTestTab.linkedBrowser;
+ // We need to proxy any callback functions using messages:
+ 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") {
+ gProxyCallbackMap.set(index, arg);
+ fnIndices.push(index);
+ return "";
+ }
+ return arg;
+ });
+ let taskArgs = {
+ methodName: prop,
+ args,
+ fnIndices,
+ };
+ return SpecialPowers.spawn(
+ browser,
+ [taskArgs],
+ async function (contentArgs) {
+ let contentWin = Cu.waiveXrays(content);
+ let callbacksCalled = 0;
+ let resolveCallbackPromise;
+ let allCallbacksCalledPromise = new Promise(
+ resolve => (resolveCallbackPromise = resolve)
+ );
+ let argumentsWithFunctions = Cu.cloneInto(
+ contentArgs.args.map((arg, index) => {
+ if (arg === "" && contentArgs.fnIndices.includes(index)) {
+ return function () {
+ callbacksCalled++;
+ SpecialPowers.spawnChrome(
+ [index, Array.from(arguments)],
+ (indexParent, argumentsParent) => {
+ // Please note that this handler only allows the callback to be used once.
+ // That means that a single gContentAPI.observer() call can't be used
+ // to observe multiple events.
+ let window = this.browsingContext.topChromeWindow;
+ let cb = window.gProxyCallbackMap.get(indexParent);
+ window.gProxyCallbackMap.delete(indexParent);
+ cb.apply(null, argumentsParent);
+ }
+ );
+ if (callbacksCalled >= contentArgs.fnIndices.length) {
+ resolveCallbackPromise();
+ }
+ };
+ }
+ return arg;
+ }),
+ content,
+ { cloneFunctions: true }
+ );
+ let rv = contentWin.Mozilla.UITour[contentArgs.methodName].apply(
+ contentWin.Mozilla.UITour,
+ argumentsWithFunctions
+ );
+ if (contentArgs.fnIndices.length) {
+ await allCallbacksCalledPromise;
+ }
+ return rv;
+ }
+ );
+ };
+ },
+ };
+ gContentAPI = new Proxy({}, UITourHandler);
+
+ await SimpleTest.promiseFocus(gTestTab.linkedBrowser);
+ callback();
+}
+
+// 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 testHttpsOrigin = "https://example.org";
+ let testHttpOrigin = "http://example.org";
+ PermissionTestUtils.add(
+ testHttpsOrigin,
+ "uitour",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ testHttpOrigin,
+ "uitour",
+ Services.perms.ALLOW_ACTION
+ );
+
+ UITour.getHighlightContainerAndMaybeCreate(window.document);
+ UITour.getTooltipAndMaybeCreate(window.document);
+
+ // 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.gContentAPI;
+ if (gTestTab) {
+ gBrowser.removeTab(gTestTab);
+ }
+ delete window.gTestTab;
+ delete window.gProxyCallbackMap;
+ Services.prefs.clearUserPref("browser.uitour.enabled");
+ PermissionTestUtils.remove(testHttpsOrigin, "uitour");
+ PermissionTestUtils.remove(testHttpOrigin, "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;
+ gProxyCallbackMap.clear();
+
+ 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) {
+ 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 = async function () {
+ await new Promise(resolve => {
+ waitForFocus(function () {
+ loadUITourTestPage(function () {
+ let funcPromise = (func() || Promise.resolve()).then(
+ () => done(true),
+ reason => {
+ ok(false, reason);
+ return done(true);
+ }
+ );
+ resolve(funcPromise);
+ });
+ });
+ });
+ };
+ Object.defineProperty(genFun, "name", {
+ configurable: true,
+ value: func.name,
+ });
+ add_task(genFun);
+}