gecko-dev/browser/components/uitour/test/head.js
Alessio Placitelli d8f23fae58 Bug 1262368 - Fix browser_UITour_heartbeat.js to work on e10s. r=MattN
MozReview-Commit-ID: 6DtCPx9wa1r
2016-04-08 23:50:00 +02:00

450 lines
16 KiB
JavaScript

"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.ownerDocument.defaultView.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.ownerDocument.defaultView.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.ownerDocument.defaultView;
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);
}