532 lines
15 KiB
JavaScript
532 lines
15 KiB
JavaScript
"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: "moz-src:///browser/components/uitour/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() {
|
|
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() {
|
|
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) {
|
|
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);
|
|
}
|